diff --git a/.github/package-filters/rs-packages-no-workflows.yml b/.github/package-filters/rs-packages-no-workflows.yml index 13da21762bc..402ea910da2 100644 --- a/.github/package-filters/rs-packages-no-workflows.yml +++ b/.github/package-filters/rs-packages-no-workflows.yml @@ -129,6 +129,10 @@ dash-context-provider: &context_provider rs-sdk-trusted-context-provider: &sdk_trusted_context_provider - packages/rs-sdk-trusted-context-provider/** + # The refresh_checkpoints example's dev-dependencies, anchored further down. + - packages/rs-dapi-client/** + - packages/dapi-grpc/** + - packages/rs-drive-proof-verifier/** - *dash_async - *context_provider - *core_proof diff --git a/.github/package-filters/rs-packages.yml b/.github/package-filters/rs-packages.yml index bcc49765fa4..c34ec37d9a0 100644 --- a/.github/package-filters/rs-packages.yml +++ b/.github/package-filters/rs-packages.yml @@ -154,6 +154,10 @@ dash-context-provider: &context_provider rs-sdk-trusted-context-provider: &sdk_trusted_context_provider - .github/workflows/tests* - packages/rs-sdk-trusted-context-provider/** + # The refresh_checkpoints example's dev-dependencies, anchored further down. + - packages/rs-dapi-client/** + - packages/dapi-grpc/** + - packages/rs-drive-proof-verifier/** - *dash_async - *context_provider - *core_proof diff --git a/Cargo.lock b/Cargo.lock index 0a62fe3a87c..623595c34a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6621,17 +6621,21 @@ name = "rs-sdk-trusted-context-provider" version = "5.0.1" dependencies = [ "arc-swap", + "dapi-grpc", "dash-async", "dash-context-provider", "dash-core-proof", + "dash-network-seeds", "data-contracts", "dpp", + "drive-proof-verifier", "futures", "gloo-timers", "hex", "js-sys", "lru", "reqwest 0.12.28", + "rs-dapi-client", "serde", "serde_json", "thiserror 2.0.18", diff --git a/packages/rs-sdk-trusted-context-provider/Cargo.toml b/packages/rs-sdk-trusted-context-provider/Cargo.toml index 67c595fe060..ab661e803fc 100644 --- a/packages/rs-sdk-trusted-context-provider/Cargo.toml +++ b/packages/rs-sdk-trusted-context-provider/Cargo.toml @@ -36,6 +36,11 @@ futures = { version = "0.3.30" } # Monotonic clock that also works on wasm32, for spacing out refreshes. web-time = "1.1" +[[example]] +name = "refresh_checkpoints" +# Its tests run with the crate's. +test = true + [features] default = [] @@ -102,6 +107,12 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.40", features = ["macros", "rt-multi-thread"] } +# The `refresh_checkpoints` example reads Platform's Core height from proved +# evonode responses. +dapi-grpc = { path = "../dapi-grpc", default-features = false, features = ["platform"] } +dash-network-seeds = { workspace = true } +drive-proof-verifier = { path = "../rs-drive-proof-verifier" } +rs-dapi-client = { path = "../rs-dapi-client" } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] # Runs the browser transport's tests under Node.js or a browser. diff --git a/packages/rs-sdk-trusted-context-provider/README.md b/packages/rs-sdk-trusted-context-provider/README.md index 67212b58059..ebd0dadc768 100644 --- a/packages/rs-sdk-trusted-context-provider/README.md +++ b/packages/rs-sdk-trusted-context-provider/README.md @@ -230,9 +230,9 @@ response must be is the SDK's policy. A proof covers every ChainLock quorum rotation between its starting state and its target, so it grows as the checkpoint ages, and a relay has to keep the -blocks back to it. Each SDK release can move the embedded checkpoints forward. -A long-lived application should persist `latest_verified_state()` and pass it -back to `with_checkpoint` on the next start, so its proofs stay short. +blocks back to it. Each release moves the embedded checkpoints forward, as +below. A long-lived application should persist `latest_verified_state()` and +pass it back to `with_checkpoint` on the next start, so its proofs stay short. ### Limitations @@ -256,7 +256,52 @@ back to `with_checkpoint` on the next start, so its proofs stay short. ### Refreshing the embedded checkpoints `checkpoints/.json` hold Core `ProofState` JSON (hashes in RPC display -order). To move one to a newer block `H`: +order). `yarn release` moves both forward with + +```sh +cargo run -p rs-sdk-trusted-context-provider --example refresh_checkpoints -- [mainnet] [testnet] +``` + +For each network it asks the public relay for two proofs from the current +checkpoint, one to the relay's tip and one to a block about a day below it +(just past the oldest active Platform quorum's mining), verifies both, and +writes the lower target as the new checkpoint. Each new checkpoint is proven +from the one it replaces, so a refresh adds no trust in the relay. It also +rewrites `tests/data/checkpoint-evidence-.json` from the proof: the target's +raw header, its coinbase transaction and the coinbase's Merkle branch. The +`checkpoints` test recomputes the block hash, the header's Merkle root and +both roots from that evidence. + +`MAINNET_QUORUM_RELAY` or `TESTNET_QUORUM_RELAY` points a network at another +relay serving `/quorums` and `/proofs`, for example a self-hosted one while +public mainnet `/proofs` returns 403 (the provider behind the Platform check +below accepts `http` only on loopback). A network that fails keeps its +checkpoint, and the release prints a warning and records it in the release +PR; an older checkpoint stays valid and only makes proofs longer. Any other +failure, including checkpoint files that cannot be written and an +interrupt, stops the release with the committed checkpoints restored, and +so does a checkpoint that no longer matches its evidence. + +The relay's quorum list only suggests the height to ask for. The new +checkpoint must be at least half a Platform quorum lifetime (about 12 hours) +below both the verified tip and the Core height Platform reports, so the +heights Platform reports after the release are above it. A relay cannot prove +a ChainLock above the real tip, and one that hides its tip only makes the +checkpoint older. Platform's Core height stops while Platform halts. It is +read from proved `getEpochsInfo` responses of up to 5 evonodes from the +network's seed list: the height is part of the state a Platform quorum signs, +and this crate's provider proves that quorum's key from the current +checkpoint. Unless a threshold of a Platform quorum signed a false block, the +assumption behind every proof, a verified height is one Platform committed, +and no evonode can report a higher one. The highest verified height is used; +if no evonode gives one, the network keeps its checkpoint. + +A refresh relies on the same thing every proof does: that no quorum on the way +from the old checkpoint signed a false ChainLock. A bad checkpoint would carry +into every later SDK build, so the release PR's reviewer should compare each +printed block hash with a block explorer at that height. + +To move a checkpoint by hand, for example when no relay can serve it: 1. Pick a height with `chainlock: true` in `getblock` on two independent Core nodes, and confirm `getblockhash H` and the block hash on a block explorer @@ -264,11 +309,9 @@ order). To move one to a newer block `H`: 2. Write the state from the block: `network` (0 mainnet, 1 testnet), `height`, `block_hash`, and the coinbase payload's `merkleRootMNList` and `merkleRootQuorums` as `masternode_root` and `quorum_root`. -3. Update `tests/data/checkpoint-evidence-.json`: the raw header - (`getblockheader false`), the raw coinbase transaction, and the - coinbase's Merkle branch (sibling hashes in wire order). The - `checkpoints` test recomputes the block hash, the header's Merkle root and - both roots from this evidence. +3. Write the evidence file: the raw header (`getblockheader false`), the + raw coinbase transaction, and the coinbase's Merkle branch (sibling hashes + in wire order). ## Implementation Details diff --git a/packages/rs-sdk-trusted-context-provider/examples/refresh_checkpoints.rs b/packages/rs-sdk-trusted-context-provider/examples/refresh_checkpoints.rs new file mode 100644 index 00000000000..d03aad137ff --- /dev/null +++ b/packages/rs-sdk-trusted-context-provider/examples/refresh_checkpoints.rs @@ -0,0 +1,1295 @@ +//! Moves the embedded checkpoints forward. `yarn release` runs it. +//! +//! ```sh +//! cargo run -p rs-sdk-trusted-context-provider --example refresh_checkpoints -- \ +//! [--report ] [mainnet] [testnet] +//! ``` +//! +//! For each network it asks the relay for two proofs from the current +//! checkpoint, one to the relay's tip and one to a block about a day below +//! it, verifies both, and writes the lower target as the new checkpoint along +//! with the evidence `tests/checkpoints.rs` checks. A new checkpoint is proven +//! from the one it replaces, so a refresh adds no trust in the relay. +//! +//! The relay's quorum list only suggests which height to ask for. The new +//! checkpoint must be at least half a Platform quorum lifetime (about 12 +//! hours) below two heights, so the Core heights Platform reports after a +//! release are above it: +//! +//! - the verified tip. A relay cannot prove a ChainLock above the real tip, +//! and one that hides its tip only makes the checkpoint older; +//! - the Core height Platform has committed, which stops while Platform +//! halts. It is read from proved responses of evonodes in the network's +//! seed list, each verified against a Platform quorum key proven from the +//! current checkpoint, so (unless a Platform quorum signed a false block) +//! no evonode can report one above Platform's own. +//! +//! `MAINNET_QUORUM_RELAY` or `TESTNET_QUORUM_RELAY` replaces a network's +//! public relay, which must serve both `/quorums` and `/proofs`. A network +//! that fails keeps its checkpoint, and once every network has run the exit +//! status is 1. `--report` appends one Markdown line per network to a file, +//! for the release PR; if that write fails, the exit status is 2. Bad +//! arguments, or no HTTP client, give 3 before any network runs. Checkpoint +//! files that cannot be written give 4. + +use dapi_grpc::platform::v0::{ + get_epochs_info_request, GetEpochsInfoRequest, GetEpochsInfoResponse, +}; +use dapi_grpc::platform::VersionedGrpcResponse; +use dash_context_provider::ContextProvider; +use dash_core_proof::{bootstrap, rpc_hex, QuorumProofChain, State, MAX_PROOF_BYTES}; +use dash_network_seeds::{PlatformLiveness, PlatformStatus, Reachability, SslStatus}; +use dpp::dashcore::sml::llmq_type::network::NetworkLLMQExt; +use dpp::dashcore::Network; +use dpp::version::{PlatformVersion, LATEST_VERSION}; +use drive_proof_verifier::types::ExtendedEpochInfos; +use drive_proof_verifier::FromProof; +use rs_dapi_client::{AddressList, DapiClient, DapiRequest, RequestSettings}; +use rs_sdk_trusted_context_provider::types::QuorumsResponse; +use rs_sdk_trusted_context_provider::{get_quorum_base_url, VerifiedHttpContextProvider}; +use serde_json::json; +use std::collections::BTreeMap; +use std::hash::{BuildHasher, RandomState}; +use std::io::Write; +use std::net::IpAddr; +use std::num::NonZeroUsize; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use std::str::FromStr; +use std::time::Duration; + +/// Largest `/quorums` body read. A real list is a few KiB. +const MAX_QUORUMS_BYTES: usize = 1 << 20; + +/// Seed evonodes asked for Platform's Core height. +const PLATFORM_NODES: usize = 5; + +/// The exit status when every network ran and at least one kept its +/// checkpoint; `release.sh` continues past only this one. +const NETWORK_KEPT: u8 = 1; +/// The exit status when the `--report` file could not be written. +const REPORT_FAILED: u8 = 2; +/// The exit status when no network ran: bad arguments, or no HTTP client. +const SETUP_FAILED: u8 = 3; +/// The exit status when a network's checkpoint files could not be written: +/// a local failure, not one of the relay's, so `release.sh` stops. +const WRITE_FAILED: u8 = 4; + +/// Why a network's checkpoint was not refreshed. +#[derive(Debug)] +enum Failure { + /// The relay, the evonodes or a proof failed, or nothing newer is far + /// enough below the tip: the network keeps its checkpoint, which stays + /// valid. + Kept(String), + /// The checkpoint files could not be written. The files are as they + /// were, unless restoring them failed too. + Write(String), +} + +impl From for Failure { + fn from(error: String) -> Self { + Self::Kept(error) + } +} + +impl std::fmt::Display for Failure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Kept(error) | Self::Write(error) => f.write_str(error), + } + } +} + +const USAGE: &str = "usage: refresh_checkpoints [--report ] [mainnet] [testnet]"; + +#[tokio::main] +async fn main() -> ExitCode { + let mut networks = Vec::new(); + let mut report = None; + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "mainnet" => networks.push(Network::Mainnet), + "testnet" => networks.push(Network::Testnet), + "--report" => match args.next() { + Some(path) => report = Some(PathBuf::from(path)), + None => { + eprintln!("{USAGE}"); + return ExitCode::from(SETUP_FAILED); + } + }, + _ => { + eprintln!("{USAGE}"); + return ExitCode::from(SETUP_FAILED); + } + } + } + if networks.is_empty() { + networks = vec![Network::Mainnet, Network::Testnet]; + } + + let client = match reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(180)) + .build() + { + Ok(client) => client, + Err(error) => { + eprintln!("cannot build an HTTP client: {error}"); + return ExitCode::from(SETUP_FAILED); + } + }; + + let mut status = ExitCode::SUCCESS; + let mut report_failed = false; + let mut write_failed = false; + for network in networks { + let line = match refresh_embedded(&client, network).await { + Ok(message) => { + println!("{network}: {message}"); + format!("- {network}: {message}") + } + // Relay text stays in a code span, so it cannot pose as + // Markdown in the release PR. + Err(Failure::Kept(error)) => { + eprintln!("{network}: checkpoint not refreshed: {error}"); + status = ExitCode::from(NETWORK_KEPT); + format!( + "- {network}: **not refreshed**, kept the previous checkpoint: `{}`", + error.replace('`', "'") + ) + } + Err(Failure::Write(error)) => { + eprintln!("{network}: cannot write the checkpoint files: {error}"); + write_failed = true; + format!( + "- {network}: **not refreshed**, the checkpoint files could not be written: `{}`", + error.replace('`', "'") + ) + } + }; + // Written as each network finishes, so the report matches the files + // even if a later network's run is cut short. + if let Some(path) = &report { + let written = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + .and_then(|mut file| writeln!(file, "{line}")); + if let Err(error) = written { + eprintln!("{}: {error}", path.display()); + report_failed = true; + } + } + } + if report_failed { + // `release.sh` stops on this, since the report no longer matches + // the files. + return ExitCode::from(REPORT_FAILED); + } + if write_failed { + return ExitCode::from(WRITE_FAILED); + } + status +} + +/// Refresh this crate's embedded checkpoint for `network`. +async fn refresh_embedded(client: &reqwest::Client, network: Network) -> Result { + let current = VerifiedHttpContextProvider::embedded_checkpoint(network) + .map_err(|error| error.to_string())?; + let variable = format!("{}_QUORUM_RELAY", network.to_string().to_uppercase()); + let relay = match std::env::var(&variable) { + Ok(relay) => relay.trim_end_matches('/').to_string(), + Err(_) => get_quorum_base_url(network, None).map_err(|error| error.to_string())?, + }; + let (platform_height, answered, asked) = + platform_core_height(network, &relay, ¤t).await?; + let message = refresh( + client, + network, + &relay, + ¤t, + platform_height, + Path::new(env!("CARGO_MANIFEST_DIR")), + ) + .await?; + Ok(format!( + "{message} ({answered} of {asked} seed evonodes gave a verified height)" + )) +} + +/// Platform's Core height, authenticated, with how many seed evonodes gave +/// one and how many were asked. +/// +/// Each evonode is asked for a proved `getEpochsInfo`. Its metadata's Core +/// height is part of the state a Platform quorum signs, and the quorum's key +/// is proven from `current` through `relay`. Unless a threshold of a Platform +/// quorum signed a false block, the assumption behind every proof, a +/// verified height is one Platform committed, so no evonode can report one +/// above Platform's own. The highest verified height is used, and one is +/// enough. +async fn platform_core_height( + network: Network, + relay: &str, + current: &State, +) -> Result<(u32, usize, usize), String> { + let provider = VerifiedHttpContextProvider::with_checkpoint( + network, + current.clone(), + vec![relay.to_string()], + NonZeroUsize::new(32).expect("nonzero"), + ) + .map_err(|error| error.to_string())?; + let settings = RequestSettings { + connect_timeout: Some(Duration::from_secs(5)), + timeout: Some(Duration::from_secs(10)), + retries: Some(0), + ..RequestSettings::default() + }; + sample_platform_height(platform_nodes(network), |uri| { + let provider = provider.clone(); + // Each node gets its own task, so a panic while verifying one node's + // answer only loses that answer. + let ask = tokio::spawn({ + let uri = uri.clone(); + async move { + let address = AddressList::from_str(&uri).map_err(|error| error.to_string())?; + let client = DapiClient::new(address, settings); + // Built twice rather than reused after `execute` takes it. + let response = epochs_request() + .execute(&client, settings) + .await + .map_err(|error| error.inner.to_string())? + .inner; + authenticated_height(network, &provider, epochs_request(), response).await + } + }); + async move { + ask.await + .map_err(|error| error.to_string()) + .and_then(|answer| answer) + .map_err(|error| format!("{uri}: {error}")) + } + }) + .await +} + +/// A small proved request: the first epoch, which always exists. +fn epochs_request() -> GetEpochsInfoRequest { + GetEpochsInfoRequest { + version: Some(get_epochs_info_request::Version::V0( + get_epochs_info_request::GetEpochsInfoRequestV0 { + start_epoch: Some(0), + count: 1, + ascending: true, + prove: true, + }, + )), + } +} + +/// The Core height in `response`'s metadata, once its proof verifies against +/// a quorum key `provider` proves. +async fn authenticated_height( + network: Network, + provider: &VerifiedHttpContextProvider, + request: GetEpochsInfoRequest, + response: GetEpochsInfoResponse, +) -> Result { + let proof = response.proof().map_err(|error| error.to_string())?; + let metadata = response.metadata().map_err(|error| error.to_string())?; + let height = metadata.core_chain_locked_height; + let quorum_hash: [u8; 32] = proof + .quorum_hash + .as_slice() + .try_into() + .map_err(|_| "a quorum hash of the wrong length".to_string())?; + // The verifier only reads the provider's cache, so prove the key first. + if let Some(fetch) = provider.fetch_quorum_public_key(proof.quorum_type, quorum_hash, height) { + fetch.await.map_err(|error| error.to_string())?; + } + // A version newer than this build's is read with the latest tables, as + // the SDK does: the quorum signs the version and the root hash, so a wrong + // reading fails the signature. An unknown older one is refused. + let version = if metadata.protocol_version > LATEST_VERSION { + PlatformVersion::latest() + } else { + PlatformVersion::get(metadata.protocol_version).map_err(|error| error.to_string())? + }; + ExtendedEpochInfos::maybe_from_proof_with_metadata( + request, response, network, version, provider, + ) + .map_err(|error| error.to_string())?; + Ok(height) +} + +/// Ask every node with `ask` and take the highest height. Each is +/// authenticated, so any answer is safe, and one is enough. +async fn sample_platform_height( + nodes: Vec, + ask: Ask, +) -> Result<(u32, usize, usize), String> +where + Ask: Fn(String) -> Answer, + Answer: std::future::Future>, +{ + let asked = nodes.len(); + let (heights, errors): (Vec<_>, Vec<_>) = futures::future::join_all(nodes.into_iter().map(ask)) + .await + .into_iter() + .partition(Result::is_ok); + let answered = heights.len(); + match heights.into_iter().flatten().max() { + Some(height) => Ok((height, answered, asked)), + None => Err(format!( + "none of {asked} seed evonodes gave an authenticated Platform Core height{}", + errors + .into_iter() + .find_map(Result::err) + .map(|error| format!(", for example {error}")) + .unwrap_or_default() + )), + } +} + +/// Up to [`PLATFORM_NODES`] seed evonodes whose Platform probes passed, taken +/// in turn from each /24 network in random order. Their answers are +/// authenticated, so the spread is only so one operator's outage leaves +/// others to ask. +fn platform_nodes(network: Network) -> Vec { + let random = RandomState::new(); + let mut networks: BTreeMap> = BTreeMap::new(); + for seed in dash_network_seeds::evo_seeds(network) { + let (Some(port), Some(platform)) = (seed.platform_http_port, &seed.platform) else { + continue; + }; + let IpAddr::V4(ip) = seed.address.ip() else { + continue; + }; + if !probe_passed(platform) { + continue; + } + let [a, b, c, _] = ip.octets(); + networks + .entry(random.hash_one([a, b, c])) + .or_default() + .push((random.hash_one(ip), format!("https://{ip}:{port}"))); + } + let mut networks: Vec<_> = networks + .into_values() + .map(|mut nodes| { + nodes.sort_unstable_by_key(|(order, _)| *order); + nodes.into_iter().map(|(_, uri)| uri) + }) + .collect(); + let mut chosen = Vec::new(); + while chosen.len() < PLATFORM_NODES { + let before = chosen.len(); + for nodes in &mut networks { + if chosen.len() < PLATFORM_NODES { + chosen.extend(nodes.next()); + } + } + if chosen.len() == before { + break; + } + } + chosen +} + +/// Whether a seed's Platform probe connected, got an HTTP answer and saw a +/// valid certificate. +fn probe_passed(platform: &PlatformStatus) -> bool { + platform.reachable == Reachability::Ok + && platform.live == PlatformLiveness::Ok + && platform.ssl == SslStatus::Valid +} + +/// Move `current` forward through `relay`, below the Core height Platform +/// reports, writing the new checkpoint and its evidence under `crate_dir`. +async fn refresh( + client: &reqwest::Client, + network: Network, + relay: &str, + current: &State, + platform_height: u32, + crate_dir: &Path, +) -> Result { + let name = network.to_string(); + let quorum_type = network.platform_type(); + let params = quorum_type.params(); + let interval = params.dkg_params.interval; + // Half a quorum lifetime: Platform's Core height trails the tip by a few + // blocks unless it halts. + let depth = interval * params.signing_active_quorum_count / 2; + + let url = format!("{relay}/quorums"); + let response = client + .get(&url) + .send() + .await + .and_then(|response| response.error_for_status()) + .map_err(|error| format!("{url}: {error}"))?; + let quorums: QuorumsResponse = + serde_json::from_slice(&read_capped(response, MAX_QUORUMS_BYTES, &url).await?) + .map_err(|error| format!("{url}: {error}"))?; + if !quorums.success { + return Err(Failure::Kept(format!("{url} reports failure"))); + } + let (Some(oldest), Some(newest)) = ( + quorums.data.iter().min_by_key(|quorum| quorum.height), + quorums.data.iter().max_by_key(|quorum| quorum.height), + ) else { + return Err(Failure::Kept(format!("{url} lists no quorums"))); + }; + + // Height 0 asks for the relay's tip; the newest quorum is active there. + let (tip, _) = prove(client, relay, network, current, 0, &newest.quorum_hash).await?; + let bound = tip.height.min(platform_height).saturating_sub(depth); + let below = format!( + "{depth} blocks below the verified tip {} and Platform's Core height {platform_height}", + tip.height + ); + if bound <= current.height { + return Ok(format!("kept {}; nothing newer is {below}", current.height)); + } + + // The oldest active quorum is in every quorum root from shortly after + // its DKG until about a day later, so a target just past its mining can + // open it. A quorum's height is where its DKG started; it is mined within + // the next DKG interval. + let height = u32::try_from(oldest.height) + .ok() + .and_then(|height| height.checked_add(interval)) + .ok_or_else(|| format!("quorum height {} is out of range", oldest.height))?; + if height <= current.height { + return Ok(format!( + "kept {}; the oldest listed quorum ({}) is not past it", + current.height, oldest.height + )); + } + if height > bound { + return Err(Failure::Kept(format!( + "the oldest listed quorum ({}) is not {below}", + oldest.height + ))); + } + let (target, chain) = + prove(client, relay, network, current, height, &oldest.quorum_hash).await?; + // The target and tip are proven, so this holds whatever the relay listed. + if target.height > bound { + return Err(Failure::Kept(format!( + "the relay answered at {}, not {below}", + target.height + ))); + } + let evidence = json!({ + "header": hex::encode(chain.target.header), + "coinbase": hex::encode(&chain.coinbase.transaction), + "coinbase_path": { + "index": chain.coinbase.path.index, + "count": chain.coinbase.path.count, + "siblings": chain.coinbase.path.siblings.iter().map(hex::encode).collect::>(), + }, + }); + + write_together( + &[ + ( + crate_dir.join(format!("tests/data/checkpoint-evidence-{name}.json")), + to_json(&evidence)?, + ), + ( + crate_dir.join(format!("checkpoints/{name}.json")), + to_json(&target)?, + ), + ], + |path, text| std::fs::write(path, text), + |from, to| std::fs::rename(from, to), + ) + .map_err(Failure::Write)?; + Ok(format!( + "{} -> {} (`{}`), verified tip {}, Platform at {platform_height}", + current.height, + target.height, + rpc_hex(&target.block_hash), + tip.height + )) +} + +/// Ask the relay for a proof from `checkpoint` to a target at or above +/// `height`, and verify it. The quorum only lets the relay build the proof; +/// its opening is not checked. +async fn prove( + client: &reqwest::Client, + relay: &str, + network: Network, + checkpoint: &State, + height: u32, + quorum_hash: &str, +) -> Result<(State, QuorumProofChain), String> { + let url = format!("{relay}/proofs"); + let response = client + .post(&url) + .json(&json!({ + "checkpoint": rpc_hex(&checkpoint.block_hash), + "height": height, + "quorumHash": quorum_hash, + "llmqType": u8::from(network.platform_type()), + "nodeCount": 0, + })) + .send() + .await + .and_then(|response| response.error_for_status()) + .map_err(|error| format!("{url}: {error}"))?; + let body = read_capped(response, MAX_PROOF_BYTES, &url).await?; + let state = bootstrap::verify(&body, checkpoint, height) + .map_err(|error| format!("the proof does not verify: {}", error.message()))? + .state() + .clone(); + // The envelope opens with the chain as a blob: a little-endian u32 + // length and the bytes. `verify` has already parsed it. + let length = u32::from_le_bytes(body[..4].try_into().expect("4 bytes")) as usize; + let chain = QuorumProofChain::decode(&body[4..4 + length]) + .map_err(|error| format!("the proof does not decode: {}", error.message()))?; + Ok((state, chain)) +} + +/// Read a body, refusing one over `limit` before buffering past it. +async fn read_capped( + mut response: reqwest::Response, + limit: usize, + url: &str, +) -> Result, String> { + let too_large = || format!("{url}: the body is over {limit} bytes"); + if response + .content_length() + .is_some_and(|length| length > limit as u64) + { + return Err(too_large()); + } + let mut body = Vec::new(); + while let Some(chunk) = response + .chunk() + .await + .map_err(|error| format!("{url}: {error}"))? + { + if chunk.len() > limit - body.len() { + return Err(too_large()); + } + body.extend_from_slice(&chunk); + } + Ok(body) +} + +fn to_json(value: &impl serde::Serialize) -> Result { + serde_json::to_string_pretty(value) + .map(|text| text + "\n") + .map_err(|error| error.to_string()) +} + +/// Replaces the files as one generation: each is written beside its +/// destination first, and if a rename fails the files already renamed get +/// their old contents back. A crash between two renames can still leave a +/// mixed pair, which the `checkpoints` test then fails. +/// `write` and `rename` are `std::fs::write` and `std::fs::rename` outside +/// tests. +fn write_together( + files: &[(PathBuf, String)], + mut write: impl FnMut(&Path, &str) -> std::io::Result<()>, + mut rename: impl FnMut(&Path, &Path) -> std::io::Result<()>, +) -> Result<(), String> { + let mut staged: Vec<(PathBuf, &PathBuf, Vec)> = Vec::with_capacity(files.len()); + for (path, text) in files { + let staging = path.with_extension("json.new"); + let written = std::fs::read(path) + .map_err(|error| format!("{}: {error}", path.display())) + .and_then(|old| { + write(&staging, text).map(|()| old).map_err(|error| { + // A failed write can leave a partial file behind. + let _ = std::fs::remove_file(&staging); + format!("{}: {error}", staging.display()) + }) + }); + match written { + Ok(old) => staged.push((staging, path, old)), + Err(error) => { + for (staging, _, _) in &staged { + let _ = std::fs::remove_file(staging); + } + return Err(error); + } + } + } + for (index, (staging, path, _)) in staged.iter().enumerate() { + if let Err(error) = rename(staging, path) { + for (_, done, old) in &staged[..index] { + if let Err(error) = std::fs::write(done, old) { + eprintln!("{}: cannot restore: {error}", done.display()); + } + } + for (staging, _, _) in &staged[index..] { + let _ = std::fs::remove_file(staging); + } + return Err(format!("{}: {error}", path.display())); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use dash_core_proof::header_hash; + use serde_json::Value; + use std::io::{BufRead, BufReader, Read}; + use std::net::TcpListener; + + /// Two real testnet proofs from one checkpoint (1,574,768): one to + /// 1,576,768, and one to the relay's tip about 5,000 blocks later. + fn fixtures() -> (State, Vec, Vec) { + let read = |json: &str| -> Value { serde_json::from_str(json).expect("fixture JSON") }; + let target = read(include_str!("../tests/data/proof-testnet.json")); + let tip = read(include_str!("../tests/data/proof-testnet-tip.json")); + assert_eq!(target["checkpoint"], tip["checkpoint"]); + let proof = |value: &Value| hex::decode(value["bootstrap_hex"].as_str().expect("hex")); + ( + serde_json::from_value(target["checkpoint"].clone()).expect("a checkpoint"), + proof(&target).expect("hex"), + proof(&tip).expect("hex"), + ) + } + + fn proven_height(proof: &[u8], checkpoint: &State) -> u32 { + bootstrap::verify(proof, checkpoint, 0) + .expect("a valid proof") + .state() + .height + } + + fn quorums(height: u32, success: bool) -> Vec { + serde_json::to_vec(&json!({ + "success": success, + "data": [{ + "quorum_hash": "00".repeat(32), + "key": "", + "height": height, + "valid_members_count": 0, + }], + })) + .expect("JSON") + } + + type Handler = Box (u16, Vec) + Send>; + + /// A relay on loopback answering each request with `handler(path, body)`. + fn relay(handler: Handler) -> String { + relay_with(handler, true) + } + + /// As [`relay`], but without a `Content-Length` when `declare_length` is + /// false: the body then ends when the connection closes. + fn relay_with(handler: Handler, declare_length: bool) -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("a loopback port"); + let address = listener.local_addr().expect("an address"); + std::thread::spawn(move || { + for stream in listener.incoming().flatten() { + let mut reader = BufReader::new(stream); + let mut line = String::new(); + if reader.read_line(&mut line).unwrap_or(0) == 0 { + continue; + } + let path = line.split_whitespace().nth(1).unwrap_or("").to_string(); + let mut length = 0; + loop { + let mut header = String::new(); + if reader.read_line(&mut header).unwrap_or(0) == 0 || header.trim().is_empty() { + break; + } + if let Some((name, value)) = header.split_once(':') { + if name.eq_ignore_ascii_case("content-length") { + length = value.trim().parse().unwrap_or(0); + } + } + } + let mut body = vec![0; length]; + if reader.read_exact(&mut body).is_err() { + continue; + } + let request = serde_json::from_slice(&body).unwrap_or(Value::Null); + let (status, body) = handler(&path, &request); + let mut stream = reader.into_inner(); + let length = if declare_length { + format!("Content-Length: {}\r\n", body.len()) + } else { + String::new() + }; + let _ = write!( + stream, + "HTTP/1.1 {status} X\r\n{length}Connection: close\r\n\r\n" + ); + let _ = stream.write_all(&body); + } + }); + format!("http://{address}") + } + + /// A crate directory holding placeholder testnet checkpoint files. + fn crate_dir(test: &str) -> PathBuf { + let dir = + std::env::temp_dir().join(format!("refresh-checkpoints-{}-{test}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + for sub in ["checkpoints", "tests/data"] { + std::fs::create_dir_all(dir.join(sub)).expect("a directory"); + } + for file in [ + "checkpoints/testnet.json", + "tests/data/checkpoint-evidence-testnet.json", + ] { + std::fs::write(dir.join(file), "old\n").expect("a placeholder"); + } + dir + } + + /// Both files hold their placeholders and no staging file is left. + fn unchanged(dir: &Path) -> bool { + [ + "checkpoints/testnet.json", + "tests/data/checkpoint-evidence-testnet.json", + ] + .iter() + .all(|file| { + std::fs::read_to_string(dir.join(file)).ok().as_deref() == Some("old\n") + && !dir.join(format!("{file}.new")).exists() + }) + } + + /// Refresh through `handler` while Platform is not the limit. + async fn run(handler: Handler, dir: &Path) -> Result { + run_at(handler, dir, u32::MAX).await + } + + /// Refresh through `handler` with Platform at `platform_height`. + async fn run_at(handler: Handler, dir: &Path, platform_height: u32) -> Result { + run_through(&relay(handler), dir, platform_height) + .await + .map_err(|failure| failure.to_string()) + } + + /// Refresh through the relay at `url` with Platform at `platform_height`. + async fn run_through(url: &str, dir: &Path, platform_height: u32) -> Result { + let (checkpoint, _, _) = fixtures(); + refresh( + &reqwest::Client::new(), + Network::Testnet, + url, + &checkpoint, + platform_height, + dir, + ) + .await + } + + /// Answers `/quorums` with one quorum at `quorum_height`, the tip ask + /// with `tip` and any other ask with `target`. + fn honest(quorum_height: u32, tip: Vec, target: Vec) -> Handler { + Box::new(move |path, request| match path { + "/quorums" => (200, quorums(quorum_height, true)), + _ if request["height"] == 0 => (200, tip.clone()), + _ => (200, target.clone()), + }) + } + + #[tokio::test] + async fn should_write_a_proven_target_below_the_verified_tip_and_its_evidence() { + let (checkpoint, target, tip) = fixtures(); + let target_height = proven_height(&target, &checkpoint); + let dir = crate_dir("success"); + + let message = run(honest(target_height - 24, tip, target.clone()), &dir) + .await + .expect("a refresh"); + assert!( + message.starts_with(&format!("1574768 -> {target_height}")), + "{message}" + ); + + let written: State = serde_json::from_str( + &std::fs::read_to_string(dir.join("checkpoints/testnet.json")).expect("a checkpoint"), + ) + .expect("a state"); + assert_eq!( + written, + *bootstrap::verify(&target, &checkpoint, 0) + .expect("valid") + .state() + ); + let evidence: Value = serde_json::from_str( + &std::fs::read_to_string(dir.join("tests/data/checkpoint-evidence-testnet.json")) + .expect("evidence"), + ) + .expect("JSON"); + let header: [u8; 80] = hex::decode(evidence["header"].as_str().expect("hex")) + .expect("hex") + .try_into() + .expect("80 bytes"); + assert_eq!(header_hash(&header), written.block_hash); + assert!(!dir.join("checkpoints/testnet.json.new").exists()); + } + + #[tokio::test] + async fn should_refuse_a_target_within_the_depth_of_the_verified_tip() { + let (checkpoint, target, tip) = fixtures(); + let dir = crate_dir("near-tip-target"); + // The relay answers every ask at its tip. + let error = run( + honest(proven_height(&target, &checkpoint) - 24, tip.clone(), tip), + &dir, + ) + .await + .expect_err("a target at the tip"); + assert!( + error.contains("not 288 blocks below the verified tip"), + "{error}" + ); + assert!(unchanged(&dir)); + } + + #[tokio::test] + async fn should_refuse_a_listed_quorum_near_the_verified_tip() { + let (checkpoint, target, tip) = fixtures(); + let tip_height = proven_height(&tip, &checkpoint); + let dir = crate_dir("near-tip-quorum"); + let error = run(honest(tip_height - 30, tip, target), &dir) + .await + .expect_err("a quorum the relay calls oldest near the tip"); + assert!(error.contains("oldest listed quorum"), "{error}"); + assert!(unchanged(&dir)); + } + + #[tokio::test] + async fn should_bound_by_a_lower_tip_when_the_relay_hides_its_own() { + let (checkpoint, target, _) = fixtures(); + let dir = crate_dir("hidden-tip"); + // The "tip" is the target itself, so it cannot be 288 blocks below. + let error = run( + honest( + proven_height(&target, &checkpoint) - 24, + target.clone(), + target, + ), + &dir, + ) + .await + .expect_err("no target far enough below a hidden tip"); + assert!(error.contains("288 blocks"), "{error}"); + assert!(unchanged(&dir)); + } + + #[tokio::test] + async fn should_refuse_an_unsuccessful_quorum_list() { + let dir = crate_dir("unsuccessful"); + let error = run(Box::new(|_, _| (200, quorums(1_576_744, false))), &dir) + .await + .expect_err("an unsuccessful list"); + assert!(error.contains("reports failure"), "{error}"); + assert!(unchanged(&dir)); + } + + #[tokio::test] + async fn should_refuse_an_oversized_proof_without_buffering_it() { + let dir = crate_dir("oversized"); + let error = run( + Box::new(|path, _| match path { + "/quorums" => (200, quorums(1_576_744, true)), + _ => (200, vec![0; MAX_PROOF_BYTES + 1]), + }), + &dir, + ) + .await + .expect_err("an oversized body"); + assert!(error.contains("over"), "{error}"); + assert!(unchanged(&dir)); + } + + /// Without a declared length, the body is refused as it streams in, once + /// it passes the limit. + #[tokio::test] + async fn should_refuse_an_oversized_proof_of_undeclared_length() { + let dir = crate_dir("oversized-streamed"); + let relay = relay_with( + Box::new(|path, _| match path { + "/quorums" => (200, quorums(1_576_744, true)), + _ => (200, vec![0; MAX_PROOF_BYTES + 1]), + }), + false, + ); + let error = run_through(&relay, &dir, u32::MAX) + .await + .expect_err("an oversized body"); + let over = format!("is over {MAX_PROOF_BYTES} bytes"); + assert!( + matches!(&error, Failure::Kept(reason) if reason.contains(&over)), + "{error:?}" + ); + assert!(unchanged(&dir)); + } + + /// Checkpoint files the refresh cannot replace, here a missing one, are + /// not a relay failure: the release must stop rather than carry on with + /// the old checkpoint. + #[tokio::test] + async fn should_report_a_checkpoint_write_failure_apart_from_relay_failures() { + let (checkpoint, target, tip) = fixtures(); + let target_height = proven_height(&target, &checkpoint); + let dir = crate_dir("unwritable"); + std::fs::remove_file(dir.join("checkpoints/testnet.json")).expect("a placeholder"); + let relay = relay(honest(target_height - 24, tip, target)); + + let error = run_through(&relay, &dir, u32::MAX) + .await + .expect_err("no checkpoint file to replace"); + + assert!(matches!(error, Failure::Write(_)), "{error:?}"); + assert!(!dir.join("checkpoints/testnet.json").exists()); + assert!(!dir + .join("tests/data/checkpoint-evidence-testnet.json.new") + .exists()); + assert_eq!( + std::fs::read_to_string(dir.join("tests/data/checkpoint-evidence-testnet.json")) + .expect("the evidence"), + "old\n" + ); + } + + #[tokio::test] + async fn should_report_a_relay_error() { + let dir = crate_dir("relay-error"); + let error = run(Box::new(|_, _| (403, b"Forbidden".to_vec())), &dir) + .await + .expect_err("a 403"); + assert!(error.contains("403"), "{error}"); + assert!(unchanged(&dir)); + } + + #[tokio::test] + async fn should_stay_below_the_core_height_of_a_halted_platform() { + let (checkpoint, target, tip) = fixtures(); + let target_height = proven_height(&target, &checkpoint); + let dir = crate_dir("halted"); + // Core went on to the tip, but Platform stopped at the target. + let error = run_at(honest(target_height - 24, tip, target), &dir, target_height) + .await + .expect_err("a target at Platform's Core height"); + assert!( + error.contains(&format!("Platform's Core height {target_height}")), + "{error}" + ); + assert!(unchanged(&dir)); + } + + #[tokio::test] + async fn should_refuse_a_proven_target_above_the_bound_set_by_platform() { + let (checkpoint, target, tip) = fixtures(); + let target_height = proven_height(&target, &checkpoint); + let dir = crate_dir("platform-bound"); + // The ask passes the bound, but the relay answers one block above it. + let error = run_at( + honest(target_height - 100, tip, target), + &dir, + target_height + 287, + ) + .await + .expect_err("a target above Platform's bound"); + assert!( + error.contains(&format!("answered at {target_height}")), + "{error}" + ); + assert!(unchanged(&dir)); + } + + #[tokio::test] + async fn should_accept_a_target_exactly_at_the_bound_set_by_platform() { + let (checkpoint, target, tip) = fixtures(); + let target_height = proven_height(&target, &checkpoint); + let dir = crate_dir("platform-boundary"); + let message = run_at( + honest(target_height - 24, tip, target), + &dir, + target_height + 288, + ) + .await + .expect("a refresh"); + assert!( + message.contains(&format!("-> {target_height}")), + "{message}" + ); + } + + #[tokio::test] + async fn should_keep_the_checkpoint_while_platform_is_not_past_it() { + let (checkpoint, target, tip) = fixtures(); + let dir = crate_dir("kept"); + let message = run_at( + honest(proven_height(&target, &checkpoint) - 24, tip, target), + &dir, + checkpoint.height + 288, + ) + .await + .expect("kept"); + assert!(message.starts_with("kept 1574768"), "{message}"); + assert!(unchanged(&dir)); + } + + /// A real proved testnet `getEpochsInfo` response, its Core height, and + /// the relay proof of its signing quorum from the fixtures' checkpoint. + fn platform_fixture() -> (u32, GetEpochsInfoResponse, Vec) { + use dapi_grpc::Message; + let value: Value = + serde_json::from_str(include_str!("../tests/data/platform-epochs-testnet.json")) + .expect("fixture JSON"); + let bytes = |key: &str| hex::decode(value[key].as_str().expect("hex")).expect("hex"); + ( + value["core_chain_locked_height"] + .as_u64() + .expect("a height") as u32, + GetEpochsInfoResponse::decode(bytes("response_hex").as_slice()).expect("a response"), + bytes("quorum_proof_hex"), + ) + } + + /// Authenticate `response` with a provider whose relay serves the + /// fixture's quorum proof. + async fn verify_platform_height(response: GetEpochsInfoResponse) -> Result { + let (checkpoint, _, _) = fixtures(); + let (_, _, quorum_proof) = platform_fixture(); + let provider = VerifiedHttpContextProvider::with_checkpoint( + Network::Testnet, + checkpoint, + vec![relay(Box::new(move |_, _| (200, quorum_proof.clone())))], + NonZeroUsize::new(32).expect("nonzero"), + ) + .expect("a provider"); + authenticated_height(Network::Testnet, &provider, epochs_request(), response).await + } + + fn metadata( + response: &mut GetEpochsInfoResponse, + ) -> &mut dapi_grpc::platform::v0::ResponseMetadata { + match response.version.as_mut().expect("a version") { + dapi_grpc::platform::v0::get_epochs_info_response::Version::V0(v0) => { + v0.metadata.as_mut().expect("metadata") + } + } + } + + #[tokio::test] + async fn should_authenticate_platforms_core_height_from_a_proved_response() { + let (height, response, _) = platform_fixture(); + assert_eq!(verify_platform_height(response).await, Ok(height)); + } + + #[tokio::test] + async fn should_refuse_a_proved_response_whose_core_height_was_changed() { + let (height, mut response, _) = platform_fixture(); + // One block higher still gets the quorum's key, but the quorum + // signed the real height. + metadata(&mut response).core_chain_locked_height = height + 1; + let error = verify_platform_height(response) + .await + .expect_err("a raised height"); + assert!(error.contains("invalid signature"), "{error}"); + } + + #[tokio::test] + async fn should_refuse_a_proof_signed_by_a_quorum_of_another_type() { + use dapi_grpc::platform::v0::get_epochs_info_response::{ + get_epochs_info_response_v0, Version, + }; + let (_, mut response, _) = platform_fixture(); + let Some(Version::V0(v0)) = response.version.as_mut() else { + panic!("a version"); + }; + let Some(get_epochs_info_response_v0::Result::Proof(proof)) = v0.result.as_mut() else { + panic!("a proof"); + }; + proof.quorum_type = 4; + let error = verify_platform_height(response) + .await + .expect_err("another quorum type"); + assert!(error.contains("is not a Platform quorum"), "{error}"); + } + + #[tokio::test] + async fn should_refuse_epochs_returned_instead_of_a_proof() { + use dapi_grpc::platform::v0::get_epochs_info_response::{ + get_epochs_info_response_v0, Version, + }; + let (_, mut response, _) = platform_fixture(); + let Some(Version::V0(v0)) = response.version.as_mut() else { + panic!("a version"); + }; + v0.result = Some(get_epochs_info_response_v0::Result::Epochs( + get_epochs_info_response_v0::EpochInfos::default(), + )); + let error = verify_platform_height(response) + .await + .expect_err("no proof"); + assert!(error.contains("proof"), "{error}"); + } + + #[tokio::test] + async fn should_refuse_a_response_without_a_proof() { + let error = verify_platform_height(GetEpochsInfoResponse { version: None }) + .await + .expect_err("no proof"); + assert!(error.contains("version"), "{error}"); + } + + #[tokio::test] + async fn should_take_the_highest_authenticated_height_and_need_only_one() { + let nodes = |count: usize| (0..count).map(|node| format!("node{node}")).collect(); + let answers = |uri: String| async move { + match uri.as_str() { + "node0" => Ok(100), + "node1" => Ok(105), + _ => Err(format!("{uri}: unreachable")), + } + }; + assert_eq!( + sample_platform_height(nodes(5), answers).await, + Ok((105, 2, 5)) + ); + let error = sample_platform_height(nodes(5), |uri: String| async move { + Err::(format!("{uri}: bad proof")) + }) + .await + .expect_err("no authenticated height"); + assert!(error.contains("none of 5"), "{error}"); + assert!(error.contains("node0: bad proof"), "{error}"); + let error = sample_platform_height(Vec::new(), |_| async { Ok(100) }) + .await + .expect_err("no nodes"); + assert!(error.contains("none of 0"), "{error}"); + } + + #[test] + fn should_ask_probed_seed_evonodes_from_every_network_block() { + let subnet = |uri: &str| { + let host = uri.trim_start_matches("https://"); + host.rsplit_once('.').map(|(subnet, _)| subnet.to_string()) + }; + for network in [Network::Mainnet, Network::Testnet] { + let nodes = platform_nodes(network); + assert!( + !nodes.is_empty() && nodes.len() <= PLATFORM_NODES, + "{network}" + ); + let mut unique = nodes.clone(); + unique.sort(); + unique.dedup(); + assert_eq!(unique.len(), nodes.len(), "{network}"); + // Every /24 with a probed node is asked before any is asked twice. + let blocks: std::collections::BTreeSet<_> = dash_network_seeds::evo_seeds(network) + .iter() + .filter(|seed| { + seed.platform_http_port.is_some() + && seed.platform.as_ref().is_some_and(probe_passed) + }) + .filter_map(|seed| subnet(&seed.address.ip().to_string())) + .collect(); + let asked: std::collections::BTreeSet<_> = + nodes.iter().filter_map(|uri| subnet(uri)).collect(); + assert_eq!(asked.len(), blocks.len().min(PLATFORM_NODES), "{network}"); + } + } + + fn files(dir: &Path) -> [(PathBuf, String); 2] { + [ + ( + dir.join("tests/data/checkpoint-evidence-testnet.json"), + "new\n".to_string(), + ), + (dir.join("checkpoints/testnet.json"), "new\n".to_string()), + ] + } + + #[test] + fn should_replace_both_files() { + let dir = crate_dir("replace"); + write_together( + &files(&dir), + |path, text| std::fs::write(path, text), + |from, to| std::fs::rename(from, to), + ) + .expect("written"); + for (path, _) in files(&dir) { + assert_eq!(std::fs::read_to_string(&path).expect("a file"), "new\n"); + assert!(!path.with_extension("json.new").exists()); + } + } + + #[test] + fn should_leave_every_file_unchanged_when_staging_one_fails() { + let dir = crate_dir("staging"); + let [first, _] = files(&dir); + let error = write_together( + &[ + first, + (dir.join("missing/testnet.json"), "new\n".to_string()), + ], + |path, text| std::fs::write(path, text), + |from, to| std::fs::rename(from, to), + ) + .expect_err("a missing destination"); + assert!(error.contains("missing"), "{error}"); + assert!(unchanged(&dir)); + } + + /// A staging write that fails partway leaves both destinations as they + /// were and removes every staging file, including the partial one. + #[test] + fn should_leave_every_file_unchanged_when_a_staging_write_fails_partway() { + let dir = crate_dir("partial-staging"); + let mut writes = 0; + let error = write_together( + &files(&dir), + |path, text| { + writes += 1; + if writes == 2 { + std::fs::write(path, &text[..1])?; + return Err(std::io::Error::other("disk full")); + } + std::fs::write(path, text) + }, + |from, to| std::fs::rename(from, to), + ) + .expect_err("a failed staging write"); + assert!(error.contains("disk full"), "{error}"); + assert_eq!(writes, 2); + assert!(unchanged(&dir)); + } + + #[test] + fn should_restore_a_replaced_file_when_a_later_rename_fails() { + let dir = crate_dir("rollback"); + let mut renames = 0; + let error = write_together( + &files(&dir), + |path, text| std::fs::write(path, text), + |from, to| { + renames += 1; + if renames == 2 { + return Err(std::io::Error::other("refused")); + } + std::fs::rename(from, to) + }, + ) + .expect_err("a failed rename"); + assert!(error.contains("refused"), "{error}"); + assert_eq!(renames, 2); + assert!(unchanged(&dir)); + } +} diff --git a/packages/rs-sdk-trusted-context-provider/src/verified_provider.rs b/packages/rs-sdk-trusted-context-provider/src/verified_provider.rs index 3e3506154dc..c4119000213 100644 --- a/packages/rs-sdk-trusted-context-provider/src/verified_provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/verified_provider.rs @@ -1928,7 +1928,9 @@ mod tests { } let mainnet = VerifiedHttpContextProvider::embedded_checkpoint(Network::Mainnet) .expect("mainnet checkpoint"); - assert_eq!(mainnet.height, 2_550_000); + // Releases move the checkpoints forward, never back. + assert_eq!(mainnet.network, ProofNetwork::Mainnet); + assert!(mainnet.height >= 2_550_000); let mismatched = VerifiedHttpContextProvider::with_checkpoint( Network::Testnet, mainnet.clone(), @@ -1953,7 +1955,13 @@ mod tests { let testnet = VerifiedHttpContextProvider::new(Network::Testnet, cache_size()) .expect("a testnet provider without network access"); - assert_eq!(testnet.checkpoint().height, 1_560_000); + assert_eq!( + *testnet.checkpoint(), + VerifiedHttpContextProvider::embedded_checkpoint(Network::Testnet) + .expect("testnet checkpoint") + ); + assert_eq!(testnet.checkpoint().network, ProofNetwork::Testnet); + assert!(testnet.checkpoint().height >= 1_560_000); assert_eq!(testnet.latest_verified_state(), *testnet.checkpoint()); assert_eq!( testnet.sources()[0].as_str(), diff --git a/packages/rs-sdk-trusted-context-provider/tests/data/platform-epochs-testnet.json b/packages/rs-sdk-trusted-context-provider/tests/data/platform-epochs-testnet.json new file mode 100644 index 00000000000..1eb146b74af --- /dev/null +++ b/packages/rs-sdk-trusted-context-provider/tests/data/platform-epochs-testnet.json @@ -0,0 +1,6 @@ +{ + "note": "A proved testnet getEpochsInfo response (epoch 0) and the relay proof of its signing quorum from the proof-testnet.json checkpoint, for refresh_checkpoints' authentication tests.", + "core_chain_locked_height": 1581959, + "response_hex": "0a890d12de0c0ab30b0100fb010001f9451c63ac339afe736dcacf6f9a9ca784779dbc72603890435f437b6d4df684020cf08f9afe7462c2aded3c6b4a62fb97ac05a49c61f6f87f843ae676966ca46f10011e82337b080e784e639a151623daf0aac0b4bf66975d8439de7343235b2f7f2a040130000f0401022a77fd000004cdb76ff56000ba2a2a1984c311d9cfbe49559cda0e6c4544d6835b859a34fa994885761b06ac1001ca180f71ab1d89e7c6cb2911b0d2603b471215234066abd57de9d0f6b5af68ab1111027d9e392ad52b7d6f2846aa583ee9ef956b3e3f02650e6c573255212662d5645b1001f3cdb7c79b1d057fd50a24e2722992aba42c0a20bada1f54e70ab821ec3889031101013000fb0454040201000006040101740000518145a42e63455d53d9fe124910d8d6e0c7617f6abbcaec65c50b739ace0a7a0402010100060401017300003d410791d0065ae497a80d7204e98c06d13737487d87e3d8ee7c8fce8ce0ef5510040201020006040101730000b6500a7feed4ef08a27067207ab9aa7135a0a503962f1a213c4a8b4036e539d111040201030006040101730000b6500a7feed4ef08a27067207ab9aa7135a0a503962f1a213c4a8b4036e539d110040201040006040101730000b6500a7feed4ef08a27067207ab9aa7135a0a503962f1a213c4a8b4036e539d102749bf1c80c2bc86463c92f050f3a4925ed1c0377b19a09c342ee9dfefcf3211110012f570f81bbaaa5698ade69272390bc9765d4b6bb242f53477d274f862e10cbfc111102de338e64c5994559c0181b11e474ea455db248acfb695d7077b130fd6e8f63ed10013fcc19108bb2cbaa45e05288c0e322177172025d03679d4b4ef880934a43d1c91102aeaf21eb72b37a33dbac8df8cb4cdf87c6404112d7b56dae0cb6976ee79b598010010883f8563615a0942dbce515ca8b207314db447de88083b5c92d7b85d10ae9081102cc567bf63c70b89743d9faea7b009f10a310878d124ce1b33745c7dfa15f54a310018dc8bc8bda067446a8b362f9e94827f6574ccc729ed93d4615b769229351e25b11024633c5011e096854946e072bfb2e049d98ae0a606286f80cacd16129f80ffa491001766b613b5560a58068173682a866a750d4b0672ae4384028682b1ec29d416f6b1102cabac2f1d6b9557ba9e5051906fb01cb51617b60e6d3081d68fdee6cd88089651001a480087dd89a2dd49e750d04a9b2df9f07c40e37da404d919ed3a9ae8ed944e91102acb129e4276fd108b87b5f90d5775a80cbe2833d4b9f78605738805d9fd4d9d01001b4aa2f893331aa88ca7eeaae980973f8c0426727858eb31a04f88fbf7ec8f2ac11029a053ee11e80b24021ca3fe9899e56edf3f5411af814ba08ce5dddb889a26096100122e446d2d3cade3f87a929f02c1e02fc88621ffd0efb9fd8915d40cc76f46dd71102a523fd5897b341ad7a030b6ee37da9058c63a7259b22fdec7625bef49725259e100196d2e9fac271a365db6c20b944804e9834863399403c8b786a7236a7bb0f399f110291c1e6658e0925ffbde7feaaaeb2be3360ee1a6a21b9e8672de84b3aba2c31521001a57d97e5794c74f528814d2319970e2602eef944b51d15798e2bd601804e2b3c11028c7013693ecf766c067ad796e48def0c388c2790d78788980141eb7422bb2f2b1001f95bfc432ca95089b94b3f11c0276e7edf399d1146204dc210962453052410251102a11c771ba7926388aae31d60e1a5c13ba6cc9c762a74aa23c44b2a403ebc4b95100143e177571c4f8f0925d21c813a5e48df6727019a4933b853a7ff6a42139bc5de110216dd9709026d1eafa8549602781fad96f688e1ac0e8253c5652b4966b0e7163e100169b131b55240a843c542078ab22a3d590fa7ee6885c4ced4192d614109541bfd1101020100004c030163000700040010a30f00030168000b000800000000000000010010030174000b000800000190c8a584a80010030176000700040000000100030178000b000800000000000003e80011110012200000004a032cd0cff0c379d6ce41b51580c46d425894b9a810c5c03f0b34687c1a6089a963751ca29f5c5339f69e06dd7de997e891e6b42d5621f1ae7b527704eb18d7e4aef14cbb5e79f7fa1b4e42fe52ad0539e2f494edf9499b69946434bdce00cbf44fc29b8e7df80a9daaeef62fdc43439a05614481384bee5f76aafb48d26d2a20747adf55c1d08eea5965213674473523a74860751e5d09465098270eb8eda92630061a2608e7ef271087c76018969c0120a5fdd1d19834280e320f646173682d746573746e65742d3531", + "quorum_proof_hex": "49310000444153484e433032017007180062bbe124d6aa0bc64fad93fd32ff9bd6b6fd805d74c5c4b11a7ea148660000008cafb54808bd6207875b674b7cabfa62f05928a85225ccf8dc2c7dd5dd8397d1c5f2dce8209b615d406568ef8b467ba2873d42a13706531760d0ae25b1ec6b5e43010000030001a36c8f433399c91765a161579f3581042422e983846375e424f47c6f0000000032ffffffffffff0332ffffffffffff03a51893239a63e5eef0983af313b6712c537d987db09433697e4e20c465650edfa27d31415ed2374643bd7770a94f4bc72af5ca8a03f64397d0ca7e68912ec6bf025df2324e4788d8f75d16ddf302ea61a1fc610571f7c92b099285e466729bbbe9a9c0b4063df1b04d6a22380440c46b65813cb857e796c5aa2b69142ec57fbe0a617bcb712c85490247b1109ea68a8ae749bbd2e8acd1bee68532a63d0e686f07ee98ca6df3e3f1a1f6f74598a1aa5eb39fd68c61165bdd6a5a983a01e17bdbba73851ed52227c6110a65df8a2f1e1796eece4a0fcbfdf4fdaf960411787a110b814c44072f5581d844373d8ed731a677ec24a09caccc1124ef76fd515980cd8006a08aaa37f35d697e879ca2038a2d2b0000006d000000077a62b476b57fec70744a81f589950ce4bd04acae0bbbc8211c1bff4f3a336ff3bd03983ba422d5979a2358f2755c6dd1cbc3944a8245c0e0739c6ed0394ef3cfcd34ac5b429af38e488cc991591a9e0f4bf855b2101aa9aa3c72aec0d2ae47a8d512a037d78330b85a937eb9e15959bcb7b6ef1dd635ca68d4bb1426109826cc10f107c74afdf43fb344c5d26ddaa381a9177e7008f8b27c244f6d0dcad3bed3b6947349c68c99dedf77f7fff75ad54c22dd8a9731421a0e195716dc082f3c2d4068b9da7d03134f7cc1fc8305a7811c3e7dd08365578f04ebdd5222a0f1b7e90e00db08180000000020d4ee16f728e3d2dbe889fea7ef248d64073eca271206694bcf22762b0400000020e2dbcc49200759d9c39a72c79bb407d3df703e72d9b20dc3777895bced19eabe6ad56ae7c5001ea52902008df875ee3138dec51f26f46d5412893b07303bb9b1cbb15350f47080440e115a6daf1ad2e2b2f16689649737a1ecefec026c8899c01783d591878cad3c9606763d8ca59e06778d4c9f319cc2b2049a55ab7c03ff14debe95a3f75b441a86cf5e5601000003000600000000000000fd49010100db081800030001399999407c2e615d1b748723ac9359485754ac5f158e2280bdf38b601700000032ffffffffffff0332ffffffffffff0394c67dcaa70a91b31d281c380e051dc97cff3779cceb8b5db7fe222a3e2f6723ef290c9da57634ef70cd4b1cfcbc817f21bd736f00c1033629054cb78ecab8db3a011600d451bacdbc7187115d88aad894ba2dbea90e446c67568e0520dad89f4cf309cc9e4b5afbefea0bbf561104deca51dea17fe05cd12c60b50ef19b1fbd17f7a5a0c1b533c2605884bf7c033d2aaffbf9c4e5a567caffc017c7fc7bbfd7b177730dd1583bc939f9fcda49d7b08aa7bf05164244af9dd667be359ae5c7a893c17b958048692a14526515a7d7da91b34d09045982fc914b56124e48cea6931960612346a9bd3aba0bddf82e6c231bfd8a79d517ca008ac258d565442bb1e121f931b500f60ec5ce90a6b3c76d847b0100000004000000021e4ca4985cb770d03fc12067e78480c170b3eacdb9b604abb6661794209c590ea8d4e7f17877174ee36a084719cf458b22d2710c8d2b62325156d049241f6c1b00001d0b180000000020884032b52f4bb1f5a1e3d965c2aefa95c3b4fe78d9dd95c2d3888e6c73000000d42a14cc705c3bf036c749c951ae2abe79a0c40ad51356d5e8489ad0c3c8067187a5d66af4c3001eaaab090092af6b4a402484a3f58dce67c0ed86b16cd7ec1de519bf26895640c126113c781f461a024cdc6d9f2cee38a3b9406656163b7ccd72168697b67f62b6bf6271f75cbcf76b3ab68b88e729f41264104e3323d4a814f33fbcf51a855f817c0f8d5e5601000003000600000000000000fd490101001c0b180003000190e99d95e1f645f2bd29a1f6e63e04758df76a2af780206f928146430d00000032ffffffffffff0332ffffffffffff03b73d638e5471eac8c27d7967738f84927b294698092b3e2886eae0c688ee8914b91943f8f76e3d76a2ed6638639d1738264d90c59b16ce249a440ba7301224f547f9c4a679313637d4b541523a2ad7d88f42583b87bbadffd25b11a27663bd13377fc40c9c40b90e59dab2902f3d506feee61c5ecf1ca5378e0c5f0cc426a07908f2983bc6020aabf7922a8d4714390b7208b99d20e7bd1cd5c5d5e612921b2dc8bc9e11eae11066f5327fc86265bda2a3683dde2fa82fe757d6c749851b0984c220dcfd7c99aff2489a68c407e70fcb796884835d00d79f7b46b119b7bb081b19889141724f5244a22b0a18370cabb9a30cbcfd06984581176468fd093f48d7fb1332535f49883d0eea5540c81e86f6010000000400000002b7a94165dacb30f0632e740b5921ad330ea845361a1dfcf83e584f35468e57cfce7bc0bf4bdb874889e7d42488da85ccbdf53ddb92d430a2730b03d655f61f5701000000002088c25e56721e4bf7bd28641de90b890a97ed8d88e05d0ab69978246f9d000000f305bea6028bedf2495f5b48bcfe5bf07586fa94e7b559bdaaef4d0f8206c5ca60a5d66a27c9001e52e906005b0d1800000000203be80b2da319518146eb2da3eefced36550dd4b076ee07daa8112abcf100000046e44661b169dba55dbd68716d1ce8c3c96cad41b9b36b493a4e425146be3d0f59d6d76a523a011ead9b0e0086ac3100239ec4fb945145811c70c4027a32cb5bd56f54a5b953d9efeb1d810693fd49accfaa8846d558358d2f45ee970d142d9ce09693d428242f67b45f1ae2fd8169bc9e60227ecb9bb2bdae407c916a9554c2f81707aa6276e5ebdca6a50f5601000003000600000000000000fd490101005b0d180003000113e78d688eeda3aa4b037bcb150c2580ff72306bfc0f1317d15bb777b100000032ffffffffffff0332ffffffffffff03a3d73c18724f5e3abb500e2a9670518f44472ab2b1280ad25bf9e3f58d211e80c4fa8034037aa30550eb7605b7b4e2c30224e116b4eb30e0f71e00e46160d9578fd8999770a88d0a8f18743bf4208d9bb0f2e85dfdc21cc58fec1902d138eb938f884ce54aea718c6434f6869be5a221b8886316254bde0d1df669bd34d0a1670af073d982fd22a82a82060368438173c199dc9f3addfaffbbecd200d9dc4090e11f8a7b470772d02dccd7c4ae6b03cb829c4f37eae9a1e8c7336096fec8824487746b3a919065615393c4f4ea5680537122cc542ee21aaa40495e5975e4be8b193c507ac95b5ff20fdaca44d8f0af7f7872ad2ded15d526a2a759d472927b0bcfe7a72db94ade09415be081d774f357010000000400000002feb2ad28b1b1c04f18cb0f9813cfc6883d07a7a894f80724df57bf37af637fd9953914d183c09cd57e2c248f0bc61e359d9c525eaf6a4dc3fff9e6de89ff126e00006d0f180000000020e6ac9e6be33e61f672a8d3dc6a68af3c15011e880e232fe37a5266b13a000000e0f214c16568f276049465f28d6cc1a6ba54703277a7657714970d6117cf05db28fdd86a693d011e45b2020082887c682c2940a8427c33784cf5b9ab68001d0b3ae43f9fbee3253acaadcd4a868969fe5622619f2cda2007d2d1baaf14c5dc8b70c997a8a0287ef6957cb26af8508e597008cba73e3872921c5187276f6b086e57ccaa648b2193c4d2ecf2015601000003000600000000000000fd490101006b0f180003000135cfd63f1f955afe02842849a6711d8c44a1f609bf7a1d07c30ab868e300000032ffffffffffff0332ffffffffffff03a09ad2beec622c017f3a3a1ab0968e008302a37cfe9aeb3e0f1eaceb0b630f984044592d7a272bc5f4dcb664fedb19863b0aef37ee5d597e1985a755e523feec642801ba5041fad6b4c75ba2cf59fac0841e043e3cdd976f3edad1d23c7856111b9008753e8a106513d77865abd631f6bfd096c04aaa840ec0fa952b78430f7201ecca5eb903576379a0546824ec08cc542ee5cd043a989f43f0aa19651bc75bccd0c4ec6ba77be37051822322881b22b6e86547a1aa6957b8a52f6e402d172a2a4bd8e27450001aa97397959ea22cb4dc094b04a9c6554c5a8d136462d9f6f10ebaadcd94ae69ada8e34284f70e6a290f58bbe17d1a17ab3864fc555d1d35ab9e6b096004bc809f646eabfe743d5aad010000000800000003e3094b67368c1fe6f48c8a426df3dd95bd3cc0a5ed9826b3c0178b343d8a1d41d2b78b1fb412832023225bacbcc1d7ced5c8c18e5e627432d8b1c5eb4fbff101955333026f0961e7cf4303dbd455083abcece0dd00efab9fa901f4da8ad018d9020000000020ea43c0b8c89ba96f447d19608287e5006101c21315a59c5c934deed4030000001919f9794609e954f0cdbeaa195e055e2c4720eacaed53b58e683f2cd895c0efecfad86af50a011eb3280800000000201764588dbf63f9bf1d04c63ecf9b07fa45473a99dcafffcdf3df140064000000bd99f537925c7cd2144d3d74d2c2b5a6f6dac903b0771920239f52f705e3715911fdd86a3715011e4bdd060004111800000000205a8f44f3174593f01a81cdb5b73ea9bc4f01529dbdb867ce651fc05eb400000031d4116f9ac1c349eeb6e41ea5aad7fc8b704384d5aa17a7fe6c533c83f26eb832ddd96a2016011ef1a60d00a42d4d99e47c8df5113de94c1114723f2dd6ffbc29f45bf62a937af990b5f9f0d1f4a1fad6528d7bebc09c0d870c2b4b07b995a3ef10381ec6709f62d8ea0ffe04e835a271b876ff21a663ca576841d6c42eb666323f425b7a3c33166643619e5601000003000600000000000000fd4901010003111800030001b0c7b2e281ab081c9ed7865d6aafc202f0ea4e0dbc8c61e87ca21fba0601000032ffffffffffff0332ffffffffffff03a3bcd6f8055c980bfd61ad614fbb23a4b004810f08b28f2f4325d61958fc1e569c0f70dbfee75d13399c6ae3df57a14900f9bbbbdada18d432e15b3a4f578c7c98cb95895ef91bbe9859a148da4392ed85d4f00abbdbc2eec4d98ec5d8879e170a31490711a18af6acc6435933d9fbf80d043f2d3cd378fe27b63f0fdec8c6d019aebaa46d27fa6a4dcb37db521a6a19e4b93d28382cf433ea4ff2aa3feac7b7cff1076f1ac77a581f98bea8be40ce2282b96cbe2111f8a67292e4f4e6febb39b8f2135105b9a509f4bb3067a8273f783a98bc0786cf360cecc7f5680b8aaebb034f6dcf43110de02b7d763cd768dbea9288bbf856c8794660649cbfc28d3c6f7da26556174e50c7cedd92c40a09346b010000000400000002e8277a2be0f7a529a1ac2d0a4ce22569e5543b1ba5c926fd80cf6ebf26543a6df8a442e481ebaf3fa7e21767dc4ee04bfd238490c0eac41b64d43b0865eba7c9010000000020f4295b4438d59c9363a206cdab2a3ccf685221db4cdc1ab58d670867a7000000e1eb2ece5e4ef3e747ab905ef092ddc4abee6029f80a4249ca4774b00872e0b61bddd96a4a1e011e606906004313180000000020a3341868203ded8202621208d2b1f15f6278dc5c4f460af1576ddf8342000000ed98aad8b99173694d06ff136e09798fba9a8226ca503efe2d1bd2ace9a835808313db6a3f74011e29340d0081654d6cc0b8e31b07e5fb439689b3097467af284d4c4b92e6a0c66e7d425b9087bc6be0aa223489693800ee670c5ccd10644060d1b91ace439e7a9780db85a641b09d12ec7f0c0955037ce0d3bdeed75a1d88b51ec0acf15736211590f954905601000003000600000000000000fd4901010043131800030001cb284f39cb5ea6e28d0354edb8f8674aa4dec8b1efa4b920bb74dd9fde00000032ffffffffffff0332ffffffffffff03a1c6a9ba74cf9268322255be1ba10b63bfccb75255e1486f457d74f8b974efdb1f5548e2565dae8299d8c98cf4a9f01d90cd51e00432000a1c56e2bb1df149e9a56137e7ce86128305c9876a656594fb9356877448e1a377f447ae0025043f7710a936e4fb8ada33c72f89ae219d148034d0c36c12b68892ec4a87501b85c6f20aa31721915cf20773f280f27179d8a05d1d1e04afb16605f1dcc9dc13158a51f85722e1d5dd827dc371c66743c12047a67c4b682f7268c151d1a6fbcdb8e26b6dae33a588cd4568f2425757461a1413d97162631aedc849093efea1598f869d061478bf3b29212cd6240ec799a7054cc623a26e0f649185d9f937e0eec6cfd4b03f4981e4c3b05e5918c00e73af055e0100000004000000022fd98250a38b49870143d550afde85fd6adc5a0339e37d4728f1b1f131c8e56ed3b6af49207220df276b20a77077ab5b8d0e8703ac8d2fcc175d4898bd90b4eb000083151800000000203bcab625167aeebd092f25e24b2c50757aee7d5dd6d6e9535ee355a4670000009927d9f340ccf24d989f2a92208cf5180128f67ae7389a569227e41715c19dc1fe58dc6a5485011e234c0b0094f5385cc7587b3659717dda8f8147a50d35fe58dc77392df2ca9ae378813f1543292ac7040c13c6dc9c5f735a6658d8050581b1217363e60f3039c2fb5ec59ad33830b0e03027c3c045af03be0383812ecb9207b1c4be8bd79ebb22851d55355601000003000600000000000000fd4901010083151800030001d3bded84637e1abbec27152b0f63ce05f64eb09dcf40b1f9dd48f6b72400000032ffefffffffff0332ffffffffffff03b96cd4c0a09afd95e1e9eb8e785b00e19918e4d748038cdf7429a0cd34a172d176f087074b295d7811c7147e04240fe383955ffbe210848bd6a0c928bacf3c2bdba86e8045d2250a453249a9189ae77097edc93344e1ed0b224507fb65388d1bed46242878de4ae9e3811969d05f6a6c26d7f92333f3d30fd0f9a57b934f0d121435be5ed53d895c9fc0430d8bf002cd21040d13cfb4330b0c9a8bdfc0107121183d38ae79f0a3b990f62a8905484d2da76b4d93450c7d2c747e49be5d1785eea9ca74ba5dca3e01feb1ae0ac248e99bd059478b5812ef35f84f70b2757dd2b9125b208fdaf6f8d8418a6bceecf247f676de955bd3f19a6d24da944187be6309d21b6893f7db3aac858c19238d4138b3010000000500000003caf77c38f3640a0f6e5099bcf786948cb0d38a80d7152ee8d049a4edb533564ca2767577c0263fcefc4cf701a8390d967400c24ac12c0210ec7264d1e5fd79049f417115da2e4a81ce7446fd98e8ad0cd1ec7f2a9b39164952356f2d77db77d600009317180000000020b11c178bf9f6f27e17befc0834b7ae023680bb7be99bb769651412d23c00000012fec1208066ef75e1321503acdca40e9c3e518c2007ba9fee16334c5869ce756176dd6a5ec8001e322c01008251768b43db38797624226710a499f71d2a09047e9731d65ce319f02d2b8090299e7c9beabdd658524896ad421e929a0152ffb557a0da9dbb1db0c9fc73609d7697f63730d471204042e03a856e58447d78e2e5ac5ade0fecfa6c6b4017bb565601000003000600000000000000fd490101009317180003000187dd321749ae4f4921cdd00a7472fc03fcdd3b061bf250a033f36be25900000032ffffffffffff0332ffffffffffff03b918ef595cabf2a05257484b85f17374fe23c3a5e31ba461ae895da39da548e72a7c4b74a9939856944f6a2ed6258c879d039aa9ab63f1c000fbf346b80fd218abccbcbbfc7897ab438da5d67c240ea995b08914b479510b835b0296a4c12fa50d0e34dcb3614f03ca8d73f8d26bad7c09278264c29a21330f32c8046180bba90c320ef31bbddf05f3dae13634479c72c24995f9ac2e7422bfecb5bd13b0d9f3a3c9f88a717427612253bb44d2a7691bb1efa1640e68de2bdf6ca331b2d43a0543c9ba4b0c1143b0449ca93455795c10757c42660e806aae00371e73f63868b90361fbe96addc143310db1d965f3cf46ba0d9fc0c85fe4bdb333fd6e4959a3779bb6c0e3d2bcc2f6b628f276cadaad090100000005000000035d51f04b38e96685a807948c4bf75f006c1aacdeb91091742b3a5c340e2866540728ae8a72c9cc9110a4edf4ee910e481566a64a96e4e04ed598f8c0aa0b13166185ad9ec8b81d5d6fcecaccd2f09978256cd16a11826efbf66625418abcc2640000a5191800000000209d41a1534c4197b85221e7569571daa991dcb269aca4ca0f562070b4d90000005bfad1d7535bdabb8e9400b407cb4580641c32400a0bd2e352b780ff6e7cf167119dde6aac00011e82020b008d883d1ddcee0143754a740dca1fe02d26c82643fc82a366fb6f9e30bbe87df9dd1ee2a09e766f52d003050db1e514b216013e3aa247852028e226e1c2eedfade001e9427818f25370e86d12cdcf5fabb2ef15d7896434e5992719233955a2725601000003000600000000000000fd49010100a3191800030001ad83bfbaf2c5eb05f8b31e2bc8aa5c48930e7c99a4220c379c984ad70201000032ffffffffffff0332ffffffffffff03a18bf4d78d2d851aaba084b2de0a6a01d5df6bc0ec10e60e405a74660afaa0e2874fd028a87dce60390fda90ef3bde90bb134a44c9550dba7a274e847eef886f3e265b22b2d99288e4c9118b52f1caeeadf71dd1f3a1ac2221b5ec3d4b12a6561d98cf4d9a7b27ce653ec733f67352f0a04e4c73ea2cce45a566362f9ef3f2a916a16168b76d6a9ebe0854693b1a85bed84a33db1699194ad338cc0698a2d7a8977f3f3f4cb01fcbcba43d8aa02630b183be8afcfda8e736bcbf075b14910bf25db3b5b6b5990c0035a40e0cba8b9cb91b082e727d2c11978c297ed10d0c6c57170565736ea956b7fe28300629b04780521ae419697bc2eee76991f238e025e80b52db6a6a22d5b778acb7dd423259000100000006000000037798fbcc82bd4a857a05cfd1835326c0ca16d40af1e86f6bcae6941c9a9e6840b234ff95c9f43c20346fd9cedb4dcfa6068381e67f23dd4aab2b65e21577162dd39102e2f27b2df06512864c573487ef7a24e227e6caed4786f25dfbcc6763e4020000000020132a5a6a06be1e95cfb73f0e241a65119d8e18d26f5a5c0a7402cb7e07000000ef087389c837b26fc6179ecda54800150ff0c0f3733a2c7b591154857747443f7d9bde6ac2fb001ee1850e0000000020127849bb54a9c7a50ca662151127de91212d0b6cdc5f384f1696e8fdd20000007e438aa343bfb642faac4c60957e8bfdbf7141bd4b0f050a1c5e4888651d6a9ef49cde6aaafe001eb0f30600e81b1800000000204b0a090d508a0e5086440e9d73ddb274021919fe683403c2f5689f861c000000dff5af6ffde7e8f948494e475722ef220ede01c8ee75bcbad9995f5a15a1904672d2df6a4ace001e589d0200a5ebb53634ad43263e0caf7054e1965c4f17a41c95e37c61c456056a4e7d5a20ec68df4b5a30d4f8e1fc3bf7f3053a2d0c0f3943e67036c0da12d1c664efbbe3a65ba685d3b1b00aecdfcf631d2d84fca654a2a9f1b7077497ac52e05ff5f6d25601000003000600000000000000fd49010100e31b18000300018bcc929986c96aa428ac63ce4bd25520244172325ef4e28759aa95351700000032ffffffffffff0332ffffffffffff0391d53a49af51a781573bfa563817cebff72ca951c4c8d8955d5a72fd80b2dfd8f5d98828104da4446adf2f84ab7aa414f6083f6e0cd37cb1f957538cbb5087dd80032dd69002f40e8740bbf7081f4a99a90051e8fb327247feeb4120c9edeefd8cdd33b206072ca577f1d80d17fc1dfe9f0f07583c604766de84d4477c5cac4a14e0e0d49c30d110c7127af43a6f0ec6ad57eea803c5817a99de595444d11237ecaf2fea6bca9925c2bb922c243bea8183080d49465a181fc48c357a78ebfd9c487e8c8f08e7aa67714ea2884854b4f3596e0af66568bdf6bad358ee02b89f3a0caa4c2e9089661d2c2fde0bc4fb3463cee9aa8c704fe4c1e03517b159966fb3efcc48cdb5f4d75202fee4e2bba0f16c010000003400000006daf10af0ed2d20e8dafb9fbb635f9e7b0803dcdd7fdf603bdc234d963df14c81ea38c0c8b415e448069cec255a74f5d6576059bf64ea2ce00259ce57e586f33e6cb9335a4d88eca612825ddb3e7d20e0bcd376251f845d85da78c8c076ed96fe4a55ac61812c9b462ab77e2ca803779d8935bf31cc2a6817e9e8d10e0f88336fcea1e08263ff92bfa098f514908e7a01dccffdd8a5c19b55a5531424a63635a9891d8d06b2990a6a256803b39abca4cc6ff29189d47752b18272a580369a5191050000000020ca8ca9a6f6554be982c5f7afb3305cdb7720cb4cbf87426e70a92d9020000000c5326c2a8be0c601ace3b66740185d955fdc4e75f32a1febfbb6c95a23d3aced17d0df6a21fa001e9f150d0000000020738da062d736d9b74f50422e3432c07255d4cc60b9a59fd611e501fbc30000003702a7755682fd0310b70792e8df461e71a2595b29e02b61ac7e70269e1f3b414ed0df6a651d011e8cb90100000000205c0b62eec35706c4599bc8a513d37bf34d012a327721f20dc01e086b460000000e727bf6e9e4b4a5087f2a222dd23e3276213de489a8fb27bdf7a7d2eea662e53fd1df6a9d25011e96f4080000000020aac54bafe96468717c24754b8682c3b2ea39a97c9640ee2ef093484ab3000000c28959791fe2cbedba0527963ca4c6921555cc9178521a2d21524d169a3866a68bd1df6ae004011ee682040000000020046260e6679dc301afc2e7cfae540816612206be91dc32624d2ceb22b60000005ddcbabf8f443cd541911b5c57154540c0aa97617ce4abb38723c2d8392e5b3055d2df6accc5001e24150200271e1800000000202de4a2b088325cf12bc0c665bd059c997972ba4c04b77cdeaf94ceb43f000000cf06c50a6fad103c3c18f56086789594df4aa7fe0168a118297763377133694dce07e16a4eea001ead65040086acc80196606443a1fa40c81cb6d9d866fb609405ff72ee6b547699f57d65867f2318ab199a37d4d7493471fa4bd9f21826fa290d20e75504f4e71a95be5be4fc9fd994a0c908c40e101989037f1c6f6450dde0c77dcd83c656c2306a75a1c95601000003000600000000000000fd49010100231e18000300016bf9f7dc25e0a1a2115bc895f15e42647cd113248af3d89dec69b94d7900000032ffffffffffff0332ffffffffffff03abaed5a687052489de5b5acb88dc0fbd59ceceb99a35050a0eb00b6d0c7b04cf2824f452e2512845c50d0d1a7ffe6438588556637938a34db77e91fd92ebb18e554dd93f4fda6cba8a327fbb91fce2b5b20bc74b2a5d837e08e5512420639060b54653aebb6deb78261ca0f485897b97e6643bdd38be6c62005d3bfdafe189d9152ee3bc105886a3116ff81485c63e346601dbefec045d924fa62649b3beb1fcdbda28a4e800128959893e8855d7d2d184fe462ef39bb707f6691df1de3eb9bc01bad363b2ccaafa44bc6522c224270adf03d481877129bbef1b248296513d7c17bf41edcefd26710cf2e0626c759818b75539922f890c133e726cd17f3e14ab3a44ca6b6bcfa3b363fd6c6b090ffce1010000000e00000004ca66204ad94053e0366b02ba8e1b0df6976c6ba2051080e155cc4a1d432aae5ef77f463be745899b399fa8dc04ece425ebaba19686037f5fa0cfdff73684874c65edcf2241031d0841a8b66876d661994e78afc54cb0daaef4e2e1c15e1d6acf2e09d7831531df3fe3ad350061970db8ba5679dffdf45032c66cb0da7897197204000000002043712683fd3e68347f31ec8e3113a22f99a347e98edad22da41f9d1d5c000000f05f5153966604c6ef32f2d24bf2ea18fa5968f039cabafa27395dfc88687d062107e16ab608011e708a0300000000207851233b063768a9098d8c817184e040452e4cf0b9e594febd89882ed1000000938c8cb2d9da0988ade4bb2b26ffddbf9c11b252064fb984e16620960486a36da407e16a44f7001e397b040000000020b4164fb94101101e9162eb803dd2a120608aab148326a1f37d0208741e00000073d795605b1fbea98704825562cc87dc5c211c37d78073dc93daeaaf370b8914a907e16a71fc001ec912000000000020567c5c4bf6a4ac7826e445df899097cc02addc1d74ebbbffbfa007f2f00000009e5e454c4947cbbe5e850bb01af8a274a26287a987e7c45357231a2d3e6d2d59c907e16af9ea001ed56d04008f1f1800000000201d3684dce1ba4ad4cace16129c6d0190010e6975db4967dd4a4836979600000008d5b11f6aab70635c1c66f0ca0bce889ff2573971ba9b1d134e2bf2825062fceecfe16a012a011ed41a0600b211d3d21e6c8bc61fdfcdb841ff8c307572df59a1b3d31372607219fc92c7f23c60bf00e27112b8f2918924b90461641198260515c3e5b466c026b758cff6097a8907a01d66c63c550f0a4ae085007d3aa20fa97e7665f0a283e12f3db3bdda5601000003000600000000000000fd490101008c1f180003000108b108d186da79b7e5576b934d6e6f62a864d176e4779305ed3927bab900000032ffffffffffff0332ffffffffffff03b9c15c6d917b2b4028ec05f54eea6b5cbcd3219c3e5064ba515d8126166f192d2bdf41e08ab227945291519ed7e0345213b9e65a8a53d82338a2fe9c6e8690f4f3ac8e8e1c2ceb047fd71c6bb6caad78a6427b489824115065b85db3b509f1bed84ecfb7098116a8672a94a0140aa82753160203a1f0d2d8cf89869ba33e331917e057998dafaebaf5d87d21da280cfa527dda8e45086bb902e6192710aad93601f538dc4dcc71c3fadd33e1325f7b178efe702871468030ed914d10877d86f3cf251d47c3ee0a639fcbc245b18127dce56244146a4fa87cdb9fe629544885e00095d5ca30fb4022e0b2fd0f2a408b59360ba42a36908911b7577eb6a5b30698d1eb1311fab79e7b4d6aee3fb278b02c010000000e0000000483ffb768525c92474e656329eda4fa33b763c6cc4c3e604a5bfc9a92d4f3c247070d20e2370a72276bf731324f58de0b09f4961f480445dc62340f8b2352561543760e8a2d7a90b4b3301d94c3860fb734805f55c2b8933fca2fc30a25f42803a493615355babf761bd612a7691f1c492357c0de771a0b12de55050e16f06f3b030000000020b5247e6a7d3162eb8b8075482b78d96a2c9c6aaf90cbe7a601a9b73fe300000051ef99e4da5c6e0a5c80295d395391002e4d6646f042c17378465c61fe6f69c56fcfe16aa829011eec7e09000000002032b11baab52b2afa2a01f4d6145325defa35fe9f992920aa3bbc9a322c000000119d5166366969dbbdfcc412ccfa8655ea17bee1e2de250cfa98b0d6d316692389cfe16a2e2a011e3b1c030000000020fb921428ee23f72da17699fcf4abeb7caed251aec7a789662581155f1f01000089d39adbd9c8a289eb8edd5a4fb97838afc62b477d0968a99871298d54a653badfcfe16af42c011eee5e020027211800000000208eda0714077f2ce1db13cfa971b4168db288e8f6d442c03924843e5e2a000000a949412717a23384241f4077937d3d7bcb8064c7a76891522fd88b5f486714ac7dabe26aac4f011e697d0200a6677efba8ed4ef619eaf6576c70585310a759d830adfe17953f129b9a57e0c74f137b66c6d5c356c6cc22b7b0b3bfc007ace1e796eb0dd02cb4bbc1d61e0525732fa8b0b6eb8fa6802f72ae1d0a3e0f94f94bed1ff5b280272234770585bcfe5601000003000600000000000000fd49010100232118000300017e29b624f441b3483ce2ec31292a076ebbd19331c15fd0d417c022028000000032ffffffffffff0332ffffffffffff03b25530b4f713daba30a0f4716641a55eb029c62657c7c16b314153566d80b0bccce7300dc35c87ae1ee9a82335f9ac198e93f9e218e52cc22de6f1c6a233622c249306305fec9ba925306336ae537356a5ed9e8e370bdb9ec5d019ba1934d8a27cb22ecb7389552debfeb079ec30edc1cae1bd5e8c2161fd21ba58616c6ea4f610fd0bcde2dbc29464b2e698f7e4bb450cbc6dcd4265bc318733fdc738427cab27667d78b59b571c19f2a207c1fc581680bb986d4d555fe64f836b16e103dc354160cae5f9889effb585e2e986d1f231768b36ab63c2eb92eef7744b97081c900fa8e9a2ec639639e997c3001d103dcd1b5d6b7d2121aa76244cea39393b7cef8745a2a1b7d5860a7d1bf962a3b2dc28010000000700000003780052641661a9cf375d6fc0fdb7de62bd33796a7006fd1ac255722241c180c6566cd03a34b17bc3cabf35f4b7f71bb987bfda13b8f75e957405592150d6a733574ae9b2bac68a9dc619076d86fc3ec214ebed95b3a33b9d0cf05761b22bc8890400000000207e36138dbf9dc768561f5b660032ddd7b659a7e3a6922bf0f87ae596230000008a5e18bba18652dc174d0de28ade6cd0b779eb478dca782906e4c4709a5b052360aae26adc81011edfd50c00000000203349e093595e059746d570cccb7ca755ffa160e86780d364fa87ff765b01000044559a9de0bd4e93864e61023e1b7997979f71c867495b43c5e7448ccfa590cd9faae26af171011ecfbb0200000000204ca85011fc962e82a81e637f9341a9bec92921ce76f2034140745ff950010000506edefe683f7e20ba34d3b7d059a1fab8962bc2c751334209fa7b0907e7cf09d4aae26a893d011e1541040000000020b9fd6bd257d528608798e7141583159236d524eb14b31e78c93548272f010000480ba18ba85e0f99b6eaca3a3ffab815c384c7cacd843162804d05276dc7e59576abe26a103e011e6bae06003b2318000000002005f615a9e28fc7be42956033ce0fa3e1d92d7d038323f7e0e3edbbe308000000d3b66f7ca97997515de8fc9c66b257c3725bdd387ee1a34589df42fea759c1004cbce36a406b2f1d0200eaa3a169e166c5c54309301c7c30bc9b5615bb4f10d5598b4fa452b0b60398f9211cd49dae94f88344f8ccad4b076e7ee86e0e975cf43fd456bd037e74037aa3ac87c7c30a205bcc2037cbb37541cc2c8ead62ef6520e8e11083302f9af689b96df55601000003000600000000000000fd4901010034231800030001f478340cdf0b302203ab060fd7b43ab6d123451dd93b21002903f5ee1d00000032ffffffffffff0332ffffffffffff039733fa17012ed7fdb5c05e5d01003cd1a7686cbe92092363effcfda56b7ff18ac494c19653ea13546807a758efcdf48b5ba1d00e5bd03956b52e8e8adff83aee46fcf96daaa6906679429fd30730fdbdb6c709902f2c6b9e90f5491208a23efb13ff4441471dbcd9cbf6776bd38f9bc89832a5b6810c82837d8ca2dfcf892d6319b723d63fe91ad24f5c8d06800edf993b79937d1c5322a6864de21acff5d8dfeca59cddad7aaf5556601b1990b47e6db122fd7e3ca97343c2d9b0a67f191b42c3aed3678d1a0b87f5aa05f75682ba1602e9a7eb22139e1a46dae0a8215c6a8b105f92824f6774bd15f425d874fb6d531c802898b187ad28ff9b92c57ed8216743d5a1bdc396123450cec2628b39aaa1010000002c000000063f2a698218f2260555ab55db92517e46bf642d567c27cda07d3446b72fc70c51f16c0dea85bcb524ce0a6d199448b3e5ba550779c7d46cb189ad6c04fc4915f1cba2006b9ab8def5c0cf9a17cb8a638eff650e27f5bc08b0c1b5f374e1f12666bccce213810c18d021f2ed8fba658dfebac15cf2cf6ab6b3750d82bb7133932e6cfa00f3c198bf8cb0000b0e75e04b1a6d02222aab26f2015708536f340b75003adde8f2c4b0a580072530d701fb1e5be9f1a59e556224ee328049f9962d7857070000000020a3535a502e8b252f233cd1d16ce0b8b453df4882785ac2bae9dfc6d3290000004accaf6d6ecaf85ab9e0b598c7b25072f1bdc2806c9489705f298fadbbb38bf4adb9e36adc9b331d016c3ef600000020ea732a400b2071307475087b8f2ecfae2c100f60493397c4579be8522d0000004ace2a0c0ab7b15ea39615f2afffaf70694f18df0aed54e316ba188c56b05c0779bae36afc37361d01e5e94f00000020970e2daf5ec00c7c2bfd7e33f902cb3245d5edb3bf9df3fa4fbb6ad40000000010593a8b52fbf96487adbaea0d2b0831462ee1211e2e6e279aa17ab6478e810ca4bae36a3de1311d0015342800000020ed32ab13eb4c33bc884059244df49af846d371f781cf25b6dcdbc28c08000000bb736087bad86f41fa7d6d1a46ec28f102fdd8bb0f73e747a2b18e79fee11ed2babae36a9b322f1d001edd02000000201dc1c27f9eef1308f5ef5b997bca04b0898d1867681fb3e6d76aaa190d000000328ca6b3778d0b4d95a0553507e0d8342a24b5c115613cab085065848950dd9d5ebbe36a71782f1d0a650e0000000020be06e9e62f4d8b6a9aa396762a478d369890542653d615f5e26c92180f000000d593f9460f888b4d86d9abc254e291e10c852ffbbd177d268bb6501d5e1446636dbbe36a88ff2f1d000f04cb0000002014653584aaef45cbf419a3e8f9d729983631618458e2f58bedb5921005000000e1c0170aec54afb8495912c0d9cc57c562e898f8addfe8c36c04d247f5ccaa398dbbe36ac4a32f1d1ea5050087231800000000203f6635a730e334226c3dcf3fc22e21c72903fcf56c749cfe50d732990b00000087edf7bd5000d1b4f499f07065c7cafeda0e0bda30aac412a19021edf52701a8d9e3e36a8efb1f1d00aa0ba5a2016e74f9426d2d563e1b508683827c37c4ea6e6f99955c619eef4d3ee5b13cc8ae0490b6e98bb03248339ca1342481072de8e213f25a84ec63b2f77c8e8c9c70787d218d0ce6be113a6656ed8922fb4b10615542443a98105140a418b9b8c89b01000003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff4a038723180429e4e36a4f534d41d22231fe62d0034c0f5622b410a7206081d9127c7baf5add515f2168e4f2de6e0400000000000000081fde8256000000000b2f54616c6f6e506f6f6c2f00000000037c558c03000000001976a914c94f9e3f6b2070deece22390eaa12239300bc74e88ac2ed5fd0300000000016a442ba706000000001976a91464f2b2b84f62d68a2cd7f7f5fb2b5aa75ef716d788ac00000000cf040087231800410190e025171ecfa413d78e896f397f2a694f37aebca2bae6547cf1d1d22be5ff00d67c159f65f681530c81996ed1954dce5c005fbd9821d76d92addde3804301b7e2a85ff6bb9fc3868ebfe6cd1157c13e2f9049757268822bd05a0a7584d7f1e99057e0e61ac6a0eff5205fc4646e4000385625169e15c3a59608b00700ce4431b961110f7ea51cafbadaa885dff960cdae4e5feaaa328400c674cb42232d6588f885496b220000000000000000000000000000000000000000000000000000000000000000000000000000030000000226df9274d4a067150f5a4d5c049d91174dccbd35b8ca58f30753233034063ad4145f1bb8298299fd366894475dc89e9a565e5bb253d33a702c68513e9be17e6501003d0100000300067c68340b3fc0c510a8b99458426dc48015b541ced679c3f0cfd02c034a00000019ffffff0119ffffff018e23923723b6299442ebe663da1ad959f25e868ebb9730ac63290036bae8ab71d01fe61853d45690611163254cc21cae1b84685c09e638f019755ede99bd3c3935e2b988f24fc7f198b8dcb4b915101483e00dc44a90a404b609098c3148f2971e19ebdc17738c160886542ac208921593832dbd35c60daa1af35c3f058926cb142322df17af3942649deaedfcd022b975f7577a39760407f19e62ab985fe061cec649a03eca7810c144f182482451fcb1d0332f50ab7fb8e1e83a26e785e3f88f887569d12259f66361127e4f2073bc3a7938a3a710b126d1abf267a5d20cd8073ff41141f332219b4c93fd1e2932e2659fe9904d7b1c14d6dcd70a768728276b406c521760d3680ba3011628fdb10a250000006d000000075e5c6b563fb9248176548efeba7febedd9704b9b2853597fb6d37d30e9658c56b26c208b27c5d5a7d2700ade3a19ef018122a4c9c421cef629727e31eb7d0db3b4ff91ba79dc5f8db3553cfb30205d220a5858b0865170d2b8f3a8d1be309cc8685ce1b476ec058663569c7ff211a4eeca53b547be03c6929cf281a464026e8facb247a09e471b1257b9a6f20d761c8e73f99dd5e4aedf3c0a8ec1854555d404919a9e53228903b535dbcd2e78a81c96ab57924f9134237925f6788588f6cb0b6df7503ce3cd6df6e5fe0979a994a02e5b3c86de040766fc89d7995dc2e1eefe" +} diff --git a/packages/rs-sdk-trusted-context-provider/tests/data/proof-testnet-tip.json b/packages/rs-sdk-trusted-context-provider/tests/data/proof-testnet-tip.json new file mode 100644 index 00000000000..0ac2beb89fd --- /dev/null +++ b/packages/rs-sdk-trusted-context-provider/tests/data/proof-testnet-tip.json @@ -0,0 +1,10 @@ +{ + "checkpoint": { + "network": 1, + "height": 1574768, + "block_hash": "0000006648a17e1ab1c4c5745d80fdb6d69bff32fd93ad4fc60baad624e1bb62", + "masternode_root": "d19783ddd57d2cdcf8cc2552a82859f062faab7c4b675b870762bd0848b5af8c", + "quorum_root": "5e6becb125aed06017530637a1423d87a27b468bef6865405d619b20e8dcf2c5" + }, + "bootstrap_hex": "402c0000444153484e433032017007180062bbe124d6aa0bc64fad93fd32ff9bd6b6fd805d74c5c4b11a7ea148660000008cafb54808bd6207875b674b7cabfa62f05928a85225ccf8dc2c7dd5dd8397d1c5f2dce8209b615d406568ef8b467ba2873d42a13706531760d0ae25b1ec6b5e43010000030001a36c8f433399c91765a161579f3581042422e983846375e424f47c6f0000000032ffffffffffff0332ffffffffffff03a51893239a63e5eef0983af313b6712c537d987db09433697e4e20c465650edfa27d31415ed2374643bd7770a94f4bc72af5ca8a03f64397d0ca7e68912ec6bf025df2324e4788d8f75d16ddf302ea61a1fc610571f7c92b099285e466729bbbe9a9c0b4063df1b04d6a22380440c46b65813cb857e796c5aa2b69142ec57fbe0a617bcb712c85490247b1109ea68a8ae749bbd2e8acd1bee68532a63d0e686f07ee98ca6df3e3f1a1f6f74598a1aa5eb39fd68c61165bdd6a5a983a01e17bdbba73851ed52227c6110a65df8a2f1e1796eece4a0fcbfdf4fdaf960411787a110b814c44072f5581d844373d8ed731a677ec24a09caccc1124ef76fd515980cd8006a08aaa37f35d697e879ca2038a2d2b0000006d000000077a62b476b57fec70744a81f589950ce4bd04acae0bbbc8211c1bff4f3a336ff3bd03983ba422d5979a2358f2755c6dd1cbc3944a8245c0e0739c6ed0394ef3cfcd34ac5b429af38e488cc991591a9e0f4bf855b2101aa9aa3c72aec0d2ae47a8d512a037d78330b85a937eb9e15959bcb7b6ef1dd635ca68d4bb1426109826cc10f107c74afdf43fb344c5d26ddaa381a9177e7008f8b27c244f6d0dcad3bed3b6947349c68c99dedf77f7fff75ad54c22dd8a9731421a0e195716dc082f3c2d4068b9da7d03134f7cc1fc8305a7811c3e7dd08365578f04ebdd5222a0f1b7e90d00db08180000000020d4ee16f728e3d2dbe889fea7ef248d64073eca271206694bcf22762b0400000020e2dbcc49200759d9c39a72c79bb407d3df703e72d9b20dc3777895bced19eabe6ad56ae7c5001ea52902008df875ee3138dec51f26f46d5412893b07303bb9b1cbb15350f47080440e115a6daf1ad2e2b2f16689649737a1ecefec026c8899c01783d591878cad3c9606763d8ca59e06778d4c9f319cc2b2049a55ab7c03ff14debe95a3f75b441a86cf5e5601000003000600000000000000fd49010100db081800030001399999407c2e615d1b748723ac9359485754ac5f158e2280bdf38b601700000032ffffffffffff0332ffffffffffff0394c67dcaa70a91b31d281c380e051dc97cff3779cceb8b5db7fe222a3e2f6723ef290c9da57634ef70cd4b1cfcbc817f21bd736f00c1033629054cb78ecab8db3a011600d451bacdbc7187115d88aad894ba2dbea90e446c67568e0520dad89f4cf309cc9e4b5afbefea0bbf561104deca51dea17fe05cd12c60b50ef19b1fbd17f7a5a0c1b533c2605884bf7c033d2aaffbf9c4e5a567caffc017c7fc7bbfd7b177730dd1583bc939f9fcda49d7b08aa7bf05164244af9dd667be359ae5c7a893c17b958048692a14526515a7d7da91b34d09045982fc914b56124e48cea6931960612346a9bd3aba0bddf82e6c231bfd8a79d517ca008ac258d565442bb1e121f931b500f60ec5ce90a6b3c76d847b0100000004000000021e4ca4985cb770d03fc12067e78480c170b3eacdb9b604abb6661794209c590ea8d4e7f17877174ee36a084719cf458b22d2710c8d2b62325156d049241f6c1b00001d0b180000000020884032b52f4bb1f5a1e3d965c2aefa95c3b4fe78d9dd95c2d3888e6c73000000d42a14cc705c3bf036c749c951ae2abe79a0c40ad51356d5e8489ad0c3c8067187a5d66af4c3001eaaab090092af6b4a402484a3f58dce67c0ed86b16cd7ec1de519bf26895640c126113c781f461a024cdc6d9f2cee38a3b9406656163b7ccd72168697b67f62b6bf6271f75cbcf76b3ab68b88e729f41264104e3323d4a814f33fbcf51a855f817c0f8d5e5601000003000600000000000000fd490101001c0b180003000190e99d95e1f645f2bd29a1f6e63e04758df76a2af780206f928146430d00000032ffffffffffff0332ffffffffffff03b73d638e5471eac8c27d7967738f84927b294698092b3e2886eae0c688ee8914b91943f8f76e3d76a2ed6638639d1738264d90c59b16ce249a440ba7301224f547f9c4a679313637d4b541523a2ad7d88f42583b87bbadffd25b11a27663bd13377fc40c9c40b90e59dab2902f3d506feee61c5ecf1ca5378e0c5f0cc426a07908f2983bc6020aabf7922a8d4714390b7208b99d20e7bd1cd5c5d5e612921b2dc8bc9e11eae11066f5327fc86265bda2a3683dde2fa82fe757d6c749851b0984c220dcfd7c99aff2489a68c407e70fcb796884835d00d79f7b46b119b7bb081b19889141724f5244a22b0a18370cabb9a30cbcfd06984581176468fd093f48d7fb1332535f49883d0eea5540c81e86f6010000000400000002b7a94165dacb30f0632e740b5921ad330ea845361a1dfcf83e584f35468e57cfce7bc0bf4bdb874889e7d42488da85ccbdf53ddb92d430a2730b03d655f61f5701000000002088c25e56721e4bf7bd28641de90b890a97ed8d88e05d0ab69978246f9d000000f305bea6028bedf2495f5b48bcfe5bf07586fa94e7b559bdaaef4d0f8206c5ca60a5d66a27c9001e52e906005b0d1800000000203be80b2da319518146eb2da3eefced36550dd4b076ee07daa8112abcf100000046e44661b169dba55dbd68716d1ce8c3c96cad41b9b36b493a4e425146be3d0f59d6d76a523a011ead9b0e0086ac3100239ec4fb945145811c70c4027a32cb5bd56f54a5b953d9efeb1d810693fd49accfaa8846d558358d2f45ee970d142d9ce09693d428242f67b45f1ae2fd8169bc9e60227ecb9bb2bdae407c916a9554c2f81707aa6276e5ebdca6a50f5601000003000600000000000000fd490101005b0d180003000113e78d688eeda3aa4b037bcb150c2580ff72306bfc0f1317d15bb777b100000032ffffffffffff0332ffffffffffff03a3d73c18724f5e3abb500e2a9670518f44472ab2b1280ad25bf9e3f58d211e80c4fa8034037aa30550eb7605b7b4e2c30224e116b4eb30e0f71e00e46160d9578fd8999770a88d0a8f18743bf4208d9bb0f2e85dfdc21cc58fec1902d138eb938f884ce54aea718c6434f6869be5a221b8886316254bde0d1df669bd34d0a1670af073d982fd22a82a82060368438173c199dc9f3addfaffbbecd200d9dc4090e11f8a7b470772d02dccd7c4ae6b03cb829c4f37eae9a1e8c7336096fec8824487746b3a919065615393c4f4ea5680537122cc542ee21aaa40495e5975e4be8b193c507ac95b5ff20fdaca44d8f0af7f7872ad2ded15d526a2a759d472927b0bcfe7a72db94ade09415be081d774f357010000000400000002feb2ad28b1b1c04f18cb0f9813cfc6883d07a7a894f80724df57bf37af637fd9953914d183c09cd57e2c248f0bc61e359d9c525eaf6a4dc3fff9e6de89ff126e00006d0f180000000020e6ac9e6be33e61f672a8d3dc6a68af3c15011e880e232fe37a5266b13a000000e0f214c16568f276049465f28d6cc1a6ba54703277a7657714970d6117cf05db28fdd86a693d011e45b2020082887c682c2940a8427c33784cf5b9ab68001d0b3ae43f9fbee3253acaadcd4a868969fe5622619f2cda2007d2d1baaf14c5dc8b70c997a8a0287ef6957cb26af8508e597008cba73e3872921c5187276f6b086e57ccaa648b2193c4d2ecf2015601000003000600000000000000fd490101006b0f180003000135cfd63f1f955afe02842849a6711d8c44a1f609bf7a1d07c30ab868e300000032ffffffffffff0332ffffffffffff03a09ad2beec622c017f3a3a1ab0968e008302a37cfe9aeb3e0f1eaceb0b630f984044592d7a272bc5f4dcb664fedb19863b0aef37ee5d597e1985a755e523feec642801ba5041fad6b4c75ba2cf59fac0841e043e3cdd976f3edad1d23c7856111b9008753e8a106513d77865abd631f6bfd096c04aaa840ec0fa952b78430f7201ecca5eb903576379a0546824ec08cc542ee5cd043a989f43f0aa19651bc75bccd0c4ec6ba77be37051822322881b22b6e86547a1aa6957b8a52f6e402d172a2a4bd8e27450001aa97397959ea22cb4dc094b04a9c6554c5a8d136462d9f6f10ebaadcd94ae69ada8e34284f70e6a290f58bbe17d1a17ab3864fc555d1d35ab9e6b096004bc809f646eabfe743d5aad010000000800000003e3094b67368c1fe6f48c8a426df3dd95bd3cc0a5ed9826b3c0178b343d8a1d41d2b78b1fb412832023225bacbcc1d7ced5c8c18e5e627432d8b1c5eb4fbff101955333026f0961e7cf4303dbd455083abcece0dd00efab9fa901f4da8ad018d9020000000020ea43c0b8c89ba96f447d19608287e5006101c21315a59c5c934deed4030000001919f9794609e954f0cdbeaa195e055e2c4720eacaed53b58e683f2cd895c0efecfad86af50a011eb3280800000000201764588dbf63f9bf1d04c63ecf9b07fa45473a99dcafffcdf3df140064000000bd99f537925c7cd2144d3d74d2c2b5a6f6dac903b0771920239f52f705e3715911fdd86a3715011e4bdd060004111800000000205a8f44f3174593f01a81cdb5b73ea9bc4f01529dbdb867ce651fc05eb400000031d4116f9ac1c349eeb6e41ea5aad7fc8b704384d5aa17a7fe6c533c83f26eb832ddd96a2016011ef1a60d00a42d4d99e47c8df5113de94c1114723f2dd6ffbc29f45bf62a937af990b5f9f0d1f4a1fad6528d7bebc09c0d870c2b4b07b995a3ef10381ec6709f62d8ea0ffe04e835a271b876ff21a663ca576841d6c42eb666323f425b7a3c33166643619e5601000003000600000000000000fd4901010003111800030001b0c7b2e281ab081c9ed7865d6aafc202f0ea4e0dbc8c61e87ca21fba0601000032ffffffffffff0332ffffffffffff03a3bcd6f8055c980bfd61ad614fbb23a4b004810f08b28f2f4325d61958fc1e569c0f70dbfee75d13399c6ae3df57a14900f9bbbbdada18d432e15b3a4f578c7c98cb95895ef91bbe9859a148da4392ed85d4f00abbdbc2eec4d98ec5d8879e170a31490711a18af6acc6435933d9fbf80d043f2d3cd378fe27b63f0fdec8c6d019aebaa46d27fa6a4dcb37db521a6a19e4b93d28382cf433ea4ff2aa3feac7b7cff1076f1ac77a581f98bea8be40ce2282b96cbe2111f8a67292e4f4e6febb39b8f2135105b9a509f4bb3067a8273f783a98bc0786cf360cecc7f5680b8aaebb034f6dcf43110de02b7d763cd768dbea9288bbf856c8794660649cbfc28d3c6f7da26556174e50c7cedd92c40a09346b010000000400000002e8277a2be0f7a529a1ac2d0a4ce22569e5543b1ba5c926fd80cf6ebf26543a6df8a442e481ebaf3fa7e21767dc4ee04bfd238490c0eac41b64d43b0865eba7c9010000000020f4295b4438d59c9363a206cdab2a3ccf685221db4cdc1ab58d670867a7000000e1eb2ece5e4ef3e747ab905ef092ddc4abee6029f80a4249ca4774b00872e0b61bddd96a4a1e011e606906004313180000000020a3341868203ded8202621208d2b1f15f6278dc5c4f460af1576ddf8342000000ed98aad8b99173694d06ff136e09798fba9a8226ca503efe2d1bd2ace9a835808313db6a3f74011e29340d0081654d6cc0b8e31b07e5fb439689b3097467af284d4c4b92e6a0c66e7d425b9087bc6be0aa223489693800ee670c5ccd10644060d1b91ace439e7a9780db85a641b09d12ec7f0c0955037ce0d3bdeed75a1d88b51ec0acf15736211590f954905601000003000600000000000000fd4901010043131800030001cb284f39cb5ea6e28d0354edb8f8674aa4dec8b1efa4b920bb74dd9fde00000032ffffffffffff0332ffffffffffff03a1c6a9ba74cf9268322255be1ba10b63bfccb75255e1486f457d74f8b974efdb1f5548e2565dae8299d8c98cf4a9f01d90cd51e00432000a1c56e2bb1df149e9a56137e7ce86128305c9876a656594fb9356877448e1a377f447ae0025043f7710a936e4fb8ada33c72f89ae219d148034d0c36c12b68892ec4a87501b85c6f20aa31721915cf20773f280f27179d8a05d1d1e04afb16605f1dcc9dc13158a51f85722e1d5dd827dc371c66743c12047a67c4b682f7268c151d1a6fbcdb8e26b6dae33a588cd4568f2425757461a1413d97162631aedc849093efea1598f869d061478bf3b29212cd6240ec799a7054cc623a26e0f649185d9f937e0eec6cfd4b03f4981e4c3b05e5918c00e73af055e0100000004000000022fd98250a38b49870143d550afde85fd6adc5a0339e37d4728f1b1f131c8e56ed3b6af49207220df276b20a77077ab5b8d0e8703ac8d2fcc175d4898bd90b4eb000083151800000000203bcab625167aeebd092f25e24b2c50757aee7d5dd6d6e9535ee355a4670000009927d9f340ccf24d989f2a92208cf5180128f67ae7389a569227e41715c19dc1fe58dc6a5485011e234c0b0094f5385cc7587b3659717dda8f8147a50d35fe58dc77392df2ca9ae378813f1543292ac7040c13c6dc9c5f735a6658d8050581b1217363e60f3039c2fb5ec59ad33830b0e03027c3c045af03be0383812ecb9207b1c4be8bd79ebb22851d55355601000003000600000000000000fd4901010083151800030001d3bded84637e1abbec27152b0f63ce05f64eb09dcf40b1f9dd48f6b72400000032ffefffffffff0332ffffffffffff03b96cd4c0a09afd95e1e9eb8e785b00e19918e4d748038cdf7429a0cd34a172d176f087074b295d7811c7147e04240fe383955ffbe210848bd6a0c928bacf3c2bdba86e8045d2250a453249a9189ae77097edc93344e1ed0b224507fb65388d1bed46242878de4ae9e3811969d05f6a6c26d7f92333f3d30fd0f9a57b934f0d121435be5ed53d895c9fc0430d8bf002cd21040d13cfb4330b0c9a8bdfc0107121183d38ae79f0a3b990f62a8905484d2da76b4d93450c7d2c747e49be5d1785eea9ca74ba5dca3e01feb1ae0ac248e99bd059478b5812ef35f84f70b2757dd2b9125b208fdaf6f8d8418a6bceecf247f676de955bd3f19a6d24da944187be6309d21b6893f7db3aac858c19238d4138b3010000000500000003caf77c38f3640a0f6e5099bcf786948cb0d38a80d7152ee8d049a4edb533564ca2767577c0263fcefc4cf701a8390d967400c24ac12c0210ec7264d1e5fd79049f417115da2e4a81ce7446fd98e8ad0cd1ec7f2a9b39164952356f2d77db77d600009317180000000020b11c178bf9f6f27e17befc0834b7ae023680bb7be99bb769651412d23c00000012fec1208066ef75e1321503acdca40e9c3e518c2007ba9fee16334c5869ce756176dd6a5ec8001e322c01008251768b43db38797624226710a499f71d2a09047e9731d65ce319f02d2b8090299e7c9beabdd658524896ad421e929a0152ffb557a0da9dbb1db0c9fc73609d7697f63730d471204042e03a856e58447d78e2e5ac5ade0fecfa6c6b4017bb565601000003000600000000000000fd490101009317180003000187dd321749ae4f4921cdd00a7472fc03fcdd3b061bf250a033f36be25900000032ffffffffffff0332ffffffffffff03b918ef595cabf2a05257484b85f17374fe23c3a5e31ba461ae895da39da548e72a7c4b74a9939856944f6a2ed6258c879d039aa9ab63f1c000fbf346b80fd218abccbcbbfc7897ab438da5d67c240ea995b08914b479510b835b0296a4c12fa50d0e34dcb3614f03ca8d73f8d26bad7c09278264c29a21330f32c8046180bba90c320ef31bbddf05f3dae13634479c72c24995f9ac2e7422bfecb5bd13b0d9f3a3c9f88a717427612253bb44d2a7691bb1efa1640e68de2bdf6ca331b2d43a0543c9ba4b0c1143b0449ca93455795c10757c42660e806aae00371e73f63868b90361fbe96addc143310db1d965f3cf46ba0d9fc0c85fe4bdb333fd6e4959a3779bb6c0e3d2bcc2f6b628f276cadaad090100000005000000035d51f04b38e96685a807948c4bf75f006c1aacdeb91091742b3a5c340e2866540728ae8a72c9cc9110a4edf4ee910e481566a64a96e4e04ed598f8c0aa0b13166185ad9ec8b81d5d6fcecaccd2f09978256cd16a11826efbf66625418abcc2640000a5191800000000209d41a1534c4197b85221e7569571daa991dcb269aca4ca0f562070b4d90000005bfad1d7535bdabb8e9400b407cb4580641c32400a0bd2e352b780ff6e7cf167119dde6aac00011e82020b008d883d1ddcee0143754a740dca1fe02d26c82643fc82a366fb6f9e30bbe87df9dd1ee2a09e766f52d003050db1e514b216013e3aa247852028e226e1c2eedfade001e9427818f25370e86d12cdcf5fabb2ef15d7896434e5992719233955a2725601000003000600000000000000fd49010100a3191800030001ad83bfbaf2c5eb05f8b31e2bc8aa5c48930e7c99a4220c379c984ad70201000032ffffffffffff0332ffffffffffff03a18bf4d78d2d851aaba084b2de0a6a01d5df6bc0ec10e60e405a74660afaa0e2874fd028a87dce60390fda90ef3bde90bb134a44c9550dba7a274e847eef886f3e265b22b2d99288e4c9118b52f1caeeadf71dd1f3a1ac2221b5ec3d4b12a6561d98cf4d9a7b27ce653ec733f67352f0a04e4c73ea2cce45a566362f9ef3f2a916a16168b76d6a9ebe0854693b1a85bed84a33db1699194ad338cc0698a2d7a8977f3f3f4cb01fcbcba43d8aa02630b183be8afcfda8e736bcbf075b14910bf25db3b5b6b5990c0035a40e0cba8b9cb91b082e727d2c11978c297ed10d0c6c57170565736ea956b7fe28300629b04780521ae419697bc2eee76991f238e025e80b52db6a6a22d5b778acb7dd423259000100000006000000037798fbcc82bd4a857a05cfd1835326c0ca16d40af1e86f6bcae6941c9a9e6840b234ff95c9f43c20346fd9cedb4dcfa6068381e67f23dd4aab2b65e21577162dd39102e2f27b2df06512864c573487ef7a24e227e6caed4786f25dfbcc6763e4020000000020132a5a6a06be1e95cfb73f0e241a65119d8e18d26f5a5c0a7402cb7e07000000ef087389c837b26fc6179ecda54800150ff0c0f3733a2c7b591154857747443f7d9bde6ac2fb001ee1850e0000000020127849bb54a9c7a50ca662151127de91212d0b6cdc5f384f1696e8fdd20000007e438aa343bfb642faac4c60957e8bfdbf7141bd4b0f050a1c5e4888651d6a9ef49cde6aaafe001eb0f30600e81b1800000000204b0a090d508a0e5086440e9d73ddb274021919fe683403c2f5689f861c000000dff5af6ffde7e8f948494e475722ef220ede01c8ee75bcbad9995f5a15a1904672d2df6a4ace001e589d0200a5ebb53634ad43263e0caf7054e1965c4f17a41c95e37c61c456056a4e7d5a20ec68df4b5a30d4f8e1fc3bf7f3053a2d0c0f3943e67036c0da12d1c664efbbe3a65ba685d3b1b00aecdfcf631d2d84fca654a2a9f1b7077497ac52e05ff5f6d25601000003000600000000000000fd49010100e31b18000300018bcc929986c96aa428ac63ce4bd25520244172325ef4e28759aa95351700000032ffffffffffff0332ffffffffffff0391d53a49af51a781573bfa563817cebff72ca951c4c8d8955d5a72fd80b2dfd8f5d98828104da4446adf2f84ab7aa414f6083f6e0cd37cb1f957538cbb5087dd80032dd69002f40e8740bbf7081f4a99a90051e8fb327247feeb4120c9edeefd8cdd33b206072ca577f1d80d17fc1dfe9f0f07583c604766de84d4477c5cac4a14e0e0d49c30d110c7127af43a6f0ec6ad57eea803c5817a99de595444d11237ecaf2fea6bca9925c2bb922c243bea8183080d49465a181fc48c357a78ebfd9c487e8c8f08e7aa67714ea2884854b4f3596e0af66568bdf6bad358ee02b89f3a0caa4c2e9089661d2c2fde0bc4fb3463cee9aa8c704fe4c1e03517b159966fb3efcc48cdb5f4d75202fee4e2bba0f16c010000003400000006daf10af0ed2d20e8dafb9fbb635f9e7b0803dcdd7fdf603bdc234d963df14c81ea38c0c8b415e448069cec255a74f5d6576059bf64ea2ce00259ce57e586f33e6cb9335a4d88eca612825ddb3e7d20e0bcd376251f845d85da78c8c076ed96fe4a55ac61812c9b462ab77e2ca803779d8935bf31cc2a6817e9e8d10e0f88336fcea1e08263ff92bfa098f514908e7a01dccffdd8a5c19b55a5531424a63635a9891d8d06b2990a6a256803b39abca4cc6ff29189d47752b18272a580369a5191050000000020ca8ca9a6f6554be982c5f7afb3305cdb7720cb4cbf87426e70a92d9020000000c5326c2a8be0c601ace3b66740185d955fdc4e75f32a1febfbb6c95a23d3aced17d0df6a21fa001e9f150d0000000020738da062d736d9b74f50422e3432c07255d4cc60b9a59fd611e501fbc30000003702a7755682fd0310b70792e8df461e71a2595b29e02b61ac7e70269e1f3b414ed0df6a651d011e8cb90100000000205c0b62eec35706c4599bc8a513d37bf34d012a327721f20dc01e086b460000000e727bf6e9e4b4a5087f2a222dd23e3276213de489a8fb27bdf7a7d2eea662e53fd1df6a9d25011e96f4080000000020aac54bafe96468717c24754b8682c3b2ea39a97c9640ee2ef093484ab3000000c28959791fe2cbedba0527963ca4c6921555cc9178521a2d21524d169a3866a68bd1df6ae004011ee682040000000020046260e6679dc301afc2e7cfae540816612206be91dc32624d2ceb22b60000005ddcbabf8f443cd541911b5c57154540c0aa97617ce4abb38723c2d8392e5b3055d2df6accc5001e24150200411e180000000020d53b8b4607711eb6f01fadf43bd5bdecbe77bb45eb78be2f2f993b4234010000a80b948fe608ea45520e5c945611f5d326c7d0cd9dd424ed720155e6bfca8eb8e816e16a0ed9011eb8850600b3ef2222c12b6145be23b67a1516c10becbe7e68ac62ba7b071e3ad05309fa82ff57bdb9dc911ee7c82fe691a9800a6d0781d3f2111a2640f25cbf64e7f8d2b9501d5ef3f2671893d5356194f038b799990838ae6ed7ff7cc332ebfbe585cefa5601000003000600000000000000fd490101003b1e18000300019d756dbcf35a0b0af108e4be71201676f85172a1e3189b35818e7b4a4c00000032ffffffffffff0332ffffffffffff03a5628d8529af81c942ebe5b54f7d1e7a6dcc1d0955a314d677cb8ca9920ae6a6081b12b380ac55b199baac5b87b50ee7e119bd0b91cd2e80dbb2641fb2a28bf4ef8284a943be8fe95bab07c5c269dccf86158919db1c1e3a86f180030cb37d9fcd9792dc5186a2ea44f09caf509b0b1ff27ec3150204e64636a0511be133c20b154dfc1b05be10bb859c75712d4589b9b1428a7522899b4f9d5794ab42600f032289bd4c6bd0fa3e7eb9cc1dc78cc5f68aae5deeb18ba24e32e5f18da6212e4f30c16855c07772ee67926bdab7b45f238d5dec7a77b44130beb894744390a89503e530ec799c6a5feb48726e8a2a7affe4aa220d4c7475cfa0965e9ddd2664cb2aef71d7a88c9b98b0ff7518f1e2827d010000000800000003d572102c9866a181ab06a90d8eadaf046590ba2bed388e923c764b76e5c1fad3a46ff7de4f75adae908706558aaf2a55a09c0ea555d13956a7129ccbe83097067f3cfebbb5d3dc855c911ca8a64e53ac4eeb3bd6e66c7e904ad0740df253433c0600000000202e8cc34d8e5b743ca8298070ac0aea9197366a0361ce04b29d9eb403350100005cb3f0770cc6f644844390745f9191dfd37ff43d741d34e47b667fa4411cf12ef914e16ae39e011e54ba0e00000000206e7de8f176aa3360adb94121a28e50202532f5f3c3bf4b834b1fb8bbc90000004851e060d1dff8d7bc6acc41722c7d1e0270d80ad569f61ae96e9c91351cbba62315e16ad199011e09b1030000000020b5fadea7c5cbec8f67437e3fa48fba939e8866eb62ff969a5cc79bba8b010000703c0416fb68c4197012925aa131309045c7bc5bb11d536e819355969c0540013715e16a4ca4011e9d980c0000000020cf7656e8180d0cebb16fd5787ef5aee62503a9e7c6c2baa47404b8556e0000003364b39d984724f1b56509b5dce09beae2708b25011de6cb326ec44a4166c9c8a815e16aa6a9011eb4dc0000000000203cba36719ca36b4ef1e59f4ade5b7c91899c62e4eaae388c45d8061d6e000000a5cc737c4b741a4697a82f752f3b66bd27f65110e7837e71a8f71958d54dbb1ca915e16abebe011edcfd0a0000000020dd7bb31b50a449c0bb15af6b1f469ab488683d176be4d965443a65d131000000d9b7ca112d870f6da258814fcd9e8f969b5b8686f4b59078da2eaa5462b03d299316e16a2cb5011e0a6804004b20180000000020142fa505451699d9d7d40a4a808f4bfa5cd43221e6d5ca7f9695547738000000531c2ff60ae682cceb43c3783d008575c47e07fcc437dd91ffb260c5a6564f37dc34e26a1178011e22cd0e00a06d521ffc3219e5924b3e6b88f4ea05acc0ef9c95ca021f00d75276fb9aec5bb458206180e96718b9df1056db190b8f0d855880f7ecd31938bf63fbd8f2d9c67fa37cabf15e0e96051870b10d13ae1165773dbf05cb61319f4d50dd3440980e5601000003000600000000000000fd490101004b201800030001941b849cc10e92a87ab079ce6a0e1e2b20d5d726f706d01fa7cf2b936200000032ffffffffffff0332ffffffffffff03a36e4e5c0c5505554e8ff6b2c9752487ba2462c8e1dc00aa6d478f90f12b4e2ef1c9ebd8ad4b3735f568a279279ae486d60f7c5a29a5690e2d0612da673fd8a0dc2e16d6f02d7e5f35b37a7a5cd49be58dd538968e28b7462581bf0310f5a5448d4c733907cd38501da8376a1ee441089529b489f944fcf9d9ae00c4e38d2eb80b261f6443dbdb24879bcb3ffffc5f9dfdc3ac8b62961c240b9fac7cbde9cb625d4a8682147f88b579e1c7ed4465b3aab49df3113f75efa49e4d0725d1a46d792635b059c00ed462cdefd463a6a42c0d61f3e1d5f6e6f1c6cf5ff9749069e0fb09c96ae324994eb63b92a7af4e3513d8d61e7a5e1684cc8e7729b733265b564c32ff99596f8f1758d93e03dd00583271010000001100000005c4432325af772414739f1653f6bed20c87e76aec7fd282cc78d7da1bba9c599db243844a34faa25128ff968096e2276afee91593c5dc9a2a1d6c7fe499a81950157cf2bfd288b9195dff259e574d349a34a24c00089dc2560cb55998940bb78ed932f0802062dd1495f2be8beea903ce10c7b8fa1d48c056f55312cf9648abab73efa92ef1fb885f5547f29d11b5b806fca5f93acd96fd7bd1ce241f5299c8d200008e22180000000020ddfe0bfd1c868e541a4891816e4c0ef5522e356b79e510d8ccffd749fa0000004ea3af4dfeba58d8255cbbe800cb9790dd96bd543f7705763f8bbd0d5c80eeee7968e36abf67011e558f0800b54e083860f5650933012268ee15aedf014a1948cfec45c198e0eec52c26c737b6f3d7f5ee6d8907ff3eec40c322ab470a85fdc9ceb680fd43d2cbece0b22299f128c22928cb8e73ec73202dbc50c8ad5f35620014948ffff5ea500481ed25655601000003000600000000000000fd490101008b221800030001fb6e4696c79d4583dc689c325749dcc80daa0dcc96575362efd88c2fb900000032ffffffffffff0332ffffffffffff038ac8d5acde692ee35f7cd635765f220406ca041ad75fb27411a7dc2edaf3141645b9e8288d105e10af4d9092bc13feb7685af0c929c9a6df8b7823e07904735069c3c6b2b2405270b8d8c569673d5c8292c88e2d0bb66b0cae7cf8c12b234a2629e461c73b5126a175b9c068ce10f46a864a79f5c135ec8b8b0c9271a7095ee509b8f1dc8cfd7a02ef6a2d9f28db8f824b93e45b7788b03e0e9887f44d6a9b2824cfd5372aff38417ba3474d0773453cb4edff1005ddb900843124e1111fd1649afd431f1b9ca2ae26f21522abfde0f9dbe714999fbb6ac9a699dd7031b717620ef565e988f3b663693de2f37204d099090a45f6aaa5895804f7241f90517bfed142fade654d0bf020410ea01fb7e8a2010000001100000005c5984744adff46309bdf8ffe901edd1fab1aff026c7b536aed5cd27a42f8cc61c1b253cdef2107eb38aed6bf7328ce17e8e7a7036e30759e4f55af70ef6fe764a2ee2ff0050d6a79ce334036c92be64b9a0fd476d9d2c68e0ffb66bcf3f5b80dfc92625c6da659a7a2d6e311880dba30483f8be3976b96018986bc5aad352df0905a1b755c11c6f7c3e9c0f33a0b4072470a09e516b80b8a28c49f36ada455730300000000205e41d3302f81d97c1b32ce9e7a2c1289de2c3c66132042f55f0f1bc85901000013e2004954a58ddd66679206126027311d463234be7014acc03ee4bee1d1dc2b6367e36ac271011e3f1f060000000020457cd9c1d50b7c4b78a188841cd702bb45a06ff5363399dfd3ad1c0519000000fd667ebeb312f7a555f973911b0e6f9ebcc403a6c34558101a02b764e85d9a160a68e36a346f011eb4800c000000002060c7a6b4653b58e04a7fa92dbb51b0fd5f00899e2cd38e4d9e3a892c400100007bab827b838aa5dbe12d99d8055afda612bad10adcdc754312bf938fdb900c277568e36a185c011e2b6603004723180000000020d4bf3af0c0f64e37c89ed6d1b4e88a25e4a13abbdcd58cef0ec4696625000000cf052ed6ddc485ae1cfd9e98058362c1f417374bf107b89a452452930cd26e991fc3e36aa103261d0134b022a23237e29d2186208ad8885aec4f73ef643c06bd3dac3b3041d77b718556d326e69ae163ba70e013d6dcf99ca69541030141a7c9c74ed6d6cf354c1e6be5223fd5968f5e7423f5a6edf10ac6e96fb027f16c9bf22c8097706461df84a6465c799b01000003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff4a03472318042bc3e36a4f534d41dfb4fedbc775b12b96cb841552842ec1d885b6356a833ddf04aa456b157c3e6f0400000000000000081fde8256000000000b2f54616c6f6e506f6f6c2f000000000374698c03000000001976a914c94f9e3f6b2070deece22390eaa12239300bc74e88ac2ed5fd0300000000016a2e67a706000000001976a91464f2b2b84f62d68a2cd7f7f5fb2b5aa75ef716d788ac00000000cf040047231800410190e025171ecfa413d78e896f397f2a694f37aebca2bae6547cf1d1d22be5b1ffc6c29f89087124a9eb1e2ef0232f5370b43e91dee21ab1442d89fbefaf4e0090d87eadd89b917f0b0f2589f450ed7429e698456fca37ba22dadaaf1b14a9259fb805ea55f185d52db548c9aa8e30bf0d9a9d4fce9638e43c12130e237abe50b12211d355e80dfd0729d2f19ef7d17d74f9df269d50e6656fdcb01433e81e49e6239f4d6a2200000000000000000000000000000000000000000000000000000000000000000000000000001d000000053bba106d34f0367b07926e88fd8a3328a8602b306de2da9811cf1be1d6a42cc5c499ec6668497153a2208d9068b40d3643fd96e8673bd4cb3c1d84a80424b1a550e06a53ba0dd39a37a32f4e7c50862e05804562901996119d54e0d00292b460a06b7b1a3c4acc469519626badd23a0fd06e6bc030681653ee90bee73637ffefadcc66c7d32dc23663e3f6e004f9a48350906999a4ce28b1988e76e8c84d7fa001003d010000030006f478340cdf0b302203ab060fd7b43ab6d123451dd93b21002903f5ee1d00000019ffffff0119ffffff0198f684f5e3095278d7f4e9708e24659e986462872d923700cfd5db226ec7d73f881b114c9127f0db9021976055194e5f783d6002b82ee62e7a6201198f93f01403d572112bf5c92c59156ab2d1052c2aaa7f8b76405f461e2c6bc1ebb8bb7d2e05f55402d2024ac23e09532c3aa607a10f0a0db3a18c89a182dd6deeb1f0f38706083450b39f683fdd86ee63caef99ad4ec77264d36f10fc4ceb69943f3c07efc27a22301c5cd36a89685bf7c7099addb662b5e3052e36d8a99a47e865e63b47c85dee8bc32e3241373544e82cbbfb4b97b784c03473f6a31cc1b3f19a8f2f3216acd47b97a94744ee34db2dadcd055e125b07fd66025f2e95366a3dd73641938158b241cbe024e1c54242be8080638d150000006d0000000732c1665fb16355d2cbf1d408c9c084eaf79e42f8cdc806071e3b1f3391d2c4e7729940ae08223abc961a5f21e5c85285ce2e4dfad647908b5285bebfa8fec3af9cfbfb759e1bbaeaed8aed1f32073dc86e53a1cd4e60992b22fb88e21743a6c01dd9244a6f55ad85e93ec12a6b2a41ec33bd053e03c86a1e0e04c5bb3ed405e726e9e94ee32062cc934f7687e182748fbc75e9b11e0c19ded5637d90d3f1fd0da6cd8da5a3f4f824d5764bfe9df825be3b8d3fdb2f5e587931a582ea18f779c5f0096edecf1e61b6a25ebf7b83665665b0fb34bda0948c38aefcd1a06531e3b8" +} diff --git a/scripts/release/release.sh b/scripts/release/release.sh index 939ab1a1999..89a51a8f108 100755 --- a/scripts/release/release.sh +++ b/scripts/release/release.sh @@ -91,6 +91,56 @@ else node "$DIR/../../packages/dashmate/scripts/generate-tenderdash-seeds.js" fi +# Move the SDK's embedded quorum-proof checkpoints forward. Each new checkpoint +# is proven from the one it replaces. A network whose relay fails keeps its +# checkpoint, which stays valid; proofs from it are only longer. +# MAINNET_QUORUM_RELAY / TESTNET_QUORUM_RELAY replace a network's public relay. +# See packages/rs-sdk-trusted-context-provider/README.md. +# The per-network results go into the release PR's description. +CHECKPOINT_REPORT=$(mktemp) +CHECKPOINTS="$DIR/../../packages/rs-sdk-trusted-context-provider" +# Put the embedded checkpoints back as committed. +restore_checkpoints() { + # Reported, not fatal under `set -e`, so a caller's message still prints. + git checkout -- "$CHECKPOINTS/checkpoints" \ + "$CHECKPOINTS/tests/data/checkpoint-evidence-mainnet.json" \ + "$CHECKPOINTS/tests/data/checkpoint-evidence-testnet.json" || + echo "ERROR: could not restore the embedded checkpoints; check them before committing." >&2 + rm -f "$CHECKPOINTS"/checkpoints/*.json.new "$CHECKPOINTS"/tests/data/*.json.new +} +stop_restoring_checkpoints() { + restore_checkpoints + echo "ERROR: $1; the embedded checkpoints are restored. Stopping." >&2 + exit 1 +} +# Until the release commit, any exit leaves the checkpoints as committed: an +# interrupt (possibly between the two files' renames) or a later failure. +trap 'stop_restoring_checkpoints "interrupted"' INT TERM +trap restore_checkpoints EXIT +if cargo run --quiet --package rs-sdk-trusted-context-provider --example refresh_checkpoints -- --report "$CHECKPOINT_REPORT"; then + : +else + case $? in + 1) + # Only an expected per-network failure, which keeps that checkpoint, + # and only once both networks have reported. + if [ "$(grep -c '^- ' "$CHECKPOINT_REPORT")" -ne 2 ]; then + stop_restoring_checkpoints "the checkpoint refresh did not report on both networks" + fi + CHECKPOINTS_KEPT=1 + echo "WARNING: an embedded quorum-proof checkpoint was not refreshed; see above." >&2 + ;; + 2) stop_restoring_checkpoints "the checkpoint refresh could not write $CHECKPOINT_REPORT" ;; + 4) stop_restoring_checkpoints "the checkpoint refresh could not write the checkpoint files" ;; + *) stop_restoring_checkpoints "the checkpoint refresh failed unexpectedly" ;; + esac +fi +# Each checkpoint must still match its evidence, even if the refresh was cut +# short between replacing the two files. +if ! cargo test --quiet --package rs-sdk-trusted-context-provider --test checkpoints; then + stop_restoring_checkpoints "an embedded checkpoint does not match its evidence" +fi + cargo metadata --format-version 1 > /dev/null NEW_PACKAGE_VERSION=$(cat $DIR/../../package.json|grep version|head -1|awk -F: '{ print $2 }'|sed 's/[", ]//g') @@ -108,6 +158,9 @@ echo "" echo "----------------------------------------------------------------" echo "Changelog base tag : $LATEST_TAG" echo "New version : $NEW_PACKAGE_VERSION" +if [ -n "${CHECKPOINTS_KEPT:-}" ]; then + echo "Quorum-proof checkpoints: NOT all refreshed (warning above)" +fi echo "Verify the base tag is the immediately-preceding release." echo "If it is wrong, abort and re-run with:" echo " yarn release -v=$NEW_PACKAGE_VERSION -c=" @@ -129,6 +182,7 @@ git checkout -b release_"$NEW_PACKAGE_VERSION" # commit changes git commit -am "chore(release): update changelog and version to $NEW_PACKAGE_VERSION" +trap - INT TERM EXIT # push changes git push -u origin release_"$NEW_PACKAGE_VERSION" @@ -141,11 +195,28 @@ else MILESTONE="v${VERSION_WITHOUT_PRERELEASE%.*}.0" fi +# The release PR's description, with each network's checkpoint result. +PR_BODY=$(mktemp) +{ + cat "$DIR/pr_description.md" + echo "" + echo "## Quorum-proof checkpoints" + echo "" + echo "Each moved checkpoint is proven from the one it replaces. Reviewers: check each new block hash at its height on a block explorer." + echo "" + if [ -s "$CHECKPOINT_REPORT" ]; then + cat "$CHECKPOINT_REPORT" + else + echo "- **The refresh did not run**; both checkpoints are unchanged (see the release log)." + fi +} > "$PR_BODY" + gh pr create --base $CURRENT_BRANCH \ --fill \ --title "chore(release): update changelog and bump version to $NEW_PACKAGE_VERSION" \ - --body-file $DIR/pr_description.md \ + --body-file "$PR_BODY" \ --milestone $MILESTONE +rm -f "$CHECKPOINT_REPORT" "$PR_BODY" echo "" echo "----------------------------------------------------------------"